blob: 3c7cd3f315fac72e94414960910bbf7e16104d24 [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):
1740 """Patchset must match."""
1741 if not self.GetPatchset():
1742 DieWithError('The patchset needs to match. Send another patchset.')
1743 try:
1744 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001745 self.GetIssue(), self.GetPatchset(), flag, value)
vapierfd77ac72016-06-16 08:33:57 -07001746 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001747 if e.code == 404:
1748 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1749 if e.code == 403:
1750 DieWithError(
1751 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1752 'match?') % (self.GetIssue(), self.GetPatchset()))
1753 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001754
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001755 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001756 """Returns an upload.RpcServer() to access this review's rietveld instance.
1757 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001758 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001759 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001760 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001761 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001762 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001763
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001764 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001765 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001766 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001767
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001768 def PatchsetSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001769 """Return the git setting that stores this change's most recent patchset."""
1770 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1771
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001772 def GetCodereviewServerSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001773 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001774 branch = self.GetBranch()
1775 if branch:
1776 return 'branch.%s.rietveldserver' % branch
1777 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001778
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001779 def _PostUnsetIssueProperties(self):
1780 """Which branch-specific properties to erase when unsetting issue."""
1781 return ['rietveldserver']
1782
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001783 def GetRieveldObjForPresubmit(self):
1784 return self.RpcServer()
1785
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001786 def SetCQState(self, new_state):
1787 props = self.GetIssueProperties()
1788 if props.get('private'):
1789 DieWithError('Cannot set-commit on private issue')
1790
1791 if new_state == _CQState.COMMIT:
1792 self.SetFlag('commit', '1')
1793 elif new_state == _CQState.NONE:
1794 self.SetFlag('commit', '0')
1795 else:
1796 raise NotImplementedError()
1797
1798
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001799 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1800 directory):
1801 # TODO(maruel): Use apply_issue.py
1802
1803 # PatchIssue should never be called with a dirty tree. It is up to the
1804 # caller to check this, but just in case we assert here since the
1805 # consequences of the caller not checking this could be dire.
1806 assert(not git_common.is_dirty_git_tree('apply'))
1807 assert(parsed_issue_arg.valid)
1808 self._changelist.issue = parsed_issue_arg.issue
1809 if parsed_issue_arg.hostname:
1810 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1811
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001812 if (isinstance(parsed_issue_arg, _RietveldParsedIssueNumberArgument) and
1813 parsed_issue_arg.patch_url):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001814 assert parsed_issue_arg.patchset
1815 patchset = parsed_issue_arg.patchset
1816 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1817 else:
1818 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1819 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1820
1821 # Switch up to the top-level directory, if necessary, in preparation for
1822 # applying the patch.
1823 top = settings.GetRelativeRoot()
1824 if top:
1825 os.chdir(top)
1826
1827 # Git patches have a/ at the beginning of source paths. We strip that out
1828 # with a sed script rather than the -p flag to patch so we can feed either
1829 # Git or svn-style patches into the same apply command.
1830 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1831 try:
1832 patch_data = subprocess2.check_output(
1833 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1834 except subprocess2.CalledProcessError:
1835 DieWithError('Git patch mungling failed.')
1836 logging.info(patch_data)
1837
1838 # We use "git apply" to apply the patch instead of "patch" so that we can
1839 # pick up file adds.
1840 # The --index flag means: also insert into the index (so we catch adds).
1841 cmd = ['git', 'apply', '--index', '-p0']
1842 if directory:
1843 cmd.extend(('--directory', directory))
1844 if reject:
1845 cmd.append('--reject')
1846 elif IsGitVersionAtLeast('1.7.12'):
1847 cmd.append('--3way')
1848 try:
1849 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
1850 stdin=patch_data, stdout=subprocess2.VOID)
1851 except subprocess2.CalledProcessError:
vapiera7fbd5a2016-06-16 09:17:49 -07001852 print('Failed to apply the patch')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001853 return 1
1854
1855 # If we had an issue, commit the current state and register the issue.
1856 if not nocommit:
1857 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
1858 'patch from issue %(i)s at patchset '
1859 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
1860 % {'i': self.GetIssue(), 'p': patchset})])
1861 self.SetIssue(self.GetIssue())
1862 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07001863 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001864 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001865 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001866 return 0
1867
1868 @staticmethod
1869 def ParseIssueURL(parsed_url):
1870 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1871 return None
1872 # Typical url: https://domain/<issue_number>[/[other]]
1873 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
1874 if match:
1875 return _RietveldParsedIssueNumberArgument(
1876 issue=int(match.group(1)),
1877 hostname=parsed_url.netloc)
1878 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
1879 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
1880 if match:
1881 return _RietveldParsedIssueNumberArgument(
1882 issue=int(match.group(1)),
1883 patchset=int(match.group(2)),
1884 hostname=parsed_url.netloc,
1885 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
1886 return None
1887
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001888 def CMDUploadChange(self, options, args, change):
1889 """Upload the patch to Rietveld."""
1890 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1891 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001892 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
1893 if options.emulate_svn_auto_props:
1894 upload_args.append('--emulate_svn_auto_props')
1895
1896 change_desc = None
1897
1898 if options.email is not None:
1899 upload_args.extend(['--email', options.email])
1900
1901 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07001902 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001903 upload_args.extend(['--title', options.title])
1904 if options.message:
1905 upload_args.extend(['--message', options.message])
1906 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07001907 print('This branch is associated with issue %s. '
1908 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001909 else:
nodirca166002016-06-27 10:59:51 -07001910 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001911 upload_args.extend(['--title', options.title])
1912 message = (options.title or options.message or
1913 CreateDescriptionFromLog(args))
1914 change_desc = ChangeDescription(message)
1915 if options.reviewers or options.tbr_owners:
1916 change_desc.update_reviewers(options.reviewers,
1917 options.tbr_owners,
1918 change)
1919 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07001920 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001921
1922 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07001923 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001924 return 1
1925
1926 upload_args.extend(['--message', change_desc.description])
1927 if change_desc.get_reviewers():
1928 upload_args.append('--reviewers=%s' % ','.join(
1929 change_desc.get_reviewers()))
1930 if options.send_mail:
1931 if not change_desc.get_reviewers():
1932 DieWithError("Must specify reviewers to send email.")
1933 upload_args.append('--send_mail')
1934
1935 # We check this before applying rietveld.private assuming that in
1936 # rietveld.cc only addresses which we can send private CLs to are listed
1937 # if rietveld.private is set, and so we should ignore rietveld.cc only
1938 # when --private is specified explicitly on the command line.
1939 if options.private:
1940 logging.warn('rietveld.cc is ignored since private flag is specified. '
1941 'You need to review and add them manually if necessary.')
1942 cc = self.GetCCListWithoutDefault()
1943 else:
1944 cc = self.GetCCList()
1945 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
1946 if cc:
1947 upload_args.extend(['--cc', cc])
1948
1949 if options.private or settings.GetDefaultPrivateFlag() == "True":
1950 upload_args.append('--private')
1951
1952 upload_args.extend(['--git_similarity', str(options.similarity)])
1953 if not options.find_copies:
1954 upload_args.extend(['--git_no_find_copies'])
1955
1956 # Include the upstream repo's URL in the change -- this is useful for
1957 # projects that have their source spread across multiple repos.
1958 remote_url = self.GetGitBaseUrlFromConfig()
1959 if not remote_url:
1960 if settings.GetIsGitSvn():
1961 remote_url = self.GetGitSvnRemoteUrl()
1962 else:
1963 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
1964 remote_url = '%s@%s' % (self.GetRemoteUrl(),
1965 self.GetUpstreamBranch().split('/')[-1])
1966 if remote_url:
1967 upload_args.extend(['--base_url', remote_url])
1968 remote, remote_branch = self.GetRemoteBranch()
1969 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
1970 settings.GetPendingRefPrefix())
1971 if target_ref:
1972 upload_args.extend(['--target_ref', target_ref])
1973
1974 # Look for dependent patchsets. See crbug.com/480453 for more details.
1975 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
1976 upstream_branch = ShortBranchName(upstream_branch)
1977 if remote is '.':
1978 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001979 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001980 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07001981 print()
1982 print('Skipping dependency patchset upload because git config '
1983 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
1984 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001985 else:
1986 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001987 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001988 auth_config=auth_config)
1989 branch_cl_issue_url = branch_cl.GetIssueURL()
1990 branch_cl_issue = branch_cl.GetIssue()
1991 branch_cl_patchset = branch_cl.GetPatchset()
1992 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
1993 upload_args.extend(
1994 ['--depends_on_patchset', '%s:%s' % (
1995 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001996 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001997 '\n'
1998 'The current branch (%s) is tracking a local branch (%s) with '
1999 'an associated CL.\n'
2000 'Adding %s/#ps%s as a dependency patchset.\n'
2001 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2002 branch_cl_patchset))
2003
2004 project = settings.GetProject()
2005 if project:
2006 upload_args.extend(['--project', project])
2007
2008 if options.cq_dry_run:
2009 upload_args.extend(['--cq_dry_run'])
2010
2011 try:
2012 upload_args = ['upload'] + upload_args + args
2013 logging.info('upload.RealMain(%s)', upload_args)
2014 issue, patchset = upload.RealMain(upload_args)
2015 issue = int(issue)
2016 patchset = int(patchset)
2017 except KeyboardInterrupt:
2018 sys.exit(1)
2019 except:
2020 # If we got an exception after the user typed a description for their
2021 # change, back up the description before re-raising.
2022 if change_desc:
2023 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2024 print('\nGot exception while uploading -- saving description to %s\n' %
2025 backup_path)
2026 backup_file = open(backup_path, 'w')
2027 backup_file.write(change_desc.description)
2028 backup_file.close()
2029 raise
2030
2031 if not self.GetIssue():
2032 self.SetIssue(issue)
2033 self.SetPatchset(patchset)
2034
2035 if options.use_commit_queue:
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002036 self.SetCQState(_CQState.COMMIT)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002037 return 0
2038
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002039
2040class _GerritChangelistImpl(_ChangelistCodereviewBase):
2041 def __init__(self, changelist, auth_config=None):
2042 # auth_config is Rietveld thing, kept here to preserve interface only.
2043 super(_GerritChangelistImpl, self).__init__(changelist)
2044 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002045 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002046 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002047 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002048
2049 def _GetGerritHost(self):
2050 # Lazy load of configs.
2051 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002052 if self._gerrit_host and '.' not in self._gerrit_host:
2053 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2054 # This happens for internal stuff http://crbug.com/614312.
2055 parsed = urlparse.urlparse(self.GetRemoteUrl())
2056 if parsed.scheme == 'sso':
2057 print('WARNING: using non https URLs for remote is likely broken\n'
2058 ' Your current remote is: %s' % self.GetRemoteUrl())
2059 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2060 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002061 return self._gerrit_host
2062
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002063 def _GetGitHost(self):
2064 """Returns git host to be used when uploading change to Gerrit."""
2065 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2066
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002067 def GetCodereviewServer(self):
2068 if not self._gerrit_server:
2069 # If we're on a branch then get the server potentially associated
2070 # with that branch.
2071 if self.GetIssue():
2072 gerrit_server_setting = self.GetCodereviewServerSetting()
2073 if gerrit_server_setting:
2074 self._gerrit_server = RunGit(['config', gerrit_server_setting],
2075 error_ok=True).strip()
2076 if self._gerrit_server:
2077 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
2078 if not self._gerrit_server:
2079 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2080 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002081 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002082 parts[0] = parts[0] + '-review'
2083 self._gerrit_host = '.'.join(parts)
2084 self._gerrit_server = 'https://%s' % self._gerrit_host
2085 return self._gerrit_server
2086
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002087 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00002088 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002089 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002090
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002091 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002092 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002093 if settings.GetGerritSkipEnsureAuthenticated():
2094 # For projects with unusual authentication schemes.
2095 # See http://crbug.com/603378.
2096 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002097 # Lazy-loader to identify Gerrit and Git hosts.
2098 if gerrit_util.GceAuthenticator.is_gce():
2099 return
2100 self.GetCodereviewServer()
2101 git_host = self._GetGitHost()
2102 assert self._gerrit_server and self._gerrit_host
2103 cookie_auth = gerrit_util.CookiesAuthenticator()
2104
2105 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2106 git_auth = cookie_auth.get_auth_header(git_host)
2107 if gerrit_auth and git_auth:
2108 if gerrit_auth == git_auth:
2109 return
2110 print((
2111 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2112 ' Check your %s or %s file for credentials of hosts:\n'
2113 ' %s\n'
2114 ' %s\n'
2115 ' %s') %
2116 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2117 git_host, self._gerrit_host,
2118 cookie_auth.get_new_password_message(git_host)))
2119 if not force:
2120 ask_for_data('If you know what you are doing, press Enter to continue, '
2121 'Ctrl+C to abort.')
2122 return
2123 else:
2124 missing = (
2125 [] if gerrit_auth else [self._gerrit_host] +
2126 [] if git_auth else [git_host])
2127 DieWithError('Credentials for the following hosts are required:\n'
2128 ' %s\n'
2129 'These are read from %s (or legacy %s)\n'
2130 '%s' % (
2131 '\n '.join(missing),
2132 cookie_auth.get_gitcookies_path(),
2133 cookie_auth.get_netrc_path(),
2134 cookie_auth.get_new_password_message(git_host)))
2135
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002136
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002137 def PatchsetSetting(self):
2138 """Return the git setting that stores this change's most recent patchset."""
2139 return 'branch.%s.gerritpatchset' % self.GetBranch()
2140
2141 def GetCodereviewServerSetting(self):
2142 """Returns the git setting that stores this change's Gerrit server."""
2143 branch = self.GetBranch()
2144 if branch:
2145 return 'branch.%s.gerritserver' % branch
2146 return None
2147
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002148 def _PostUnsetIssueProperties(self):
2149 """Which branch-specific properties to erase when unsetting issue."""
2150 return [
2151 'gerritserver',
2152 'gerritsquashhash',
2153 ]
2154
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002155 def GetRieveldObjForPresubmit(self):
2156 class ThisIsNotRietveldIssue(object):
2157 def __nonzero__(self):
2158 # This is a hack to make presubmit_support think that rietveld is not
2159 # defined, yet still ensure that calls directly result in a decent
2160 # exception message below.
2161 return False
2162
2163 def __getattr__(self, attr):
2164 print(
2165 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2166 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2167 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2168 'or use Rietveld for codereview.\n'
2169 'See also http://crbug.com/579160.' % attr)
2170 raise NotImplementedError()
2171 return ThisIsNotRietveldIssue()
2172
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002173 def GetGerritObjForPresubmit(self):
2174 return presubmit_support.GerritAccessor(self._GetGerritHost())
2175
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002176 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002177 """Apply a rough heuristic to give a simple summary of an issue's review
2178 or CQ status, assuming adherence to a common workflow.
2179
2180 Returns None if no issue for this branch, or one of the following keywords:
2181 * 'error' - error from review tool (including deleted issues)
2182 * 'unsent' - no reviewers added
2183 * 'waiting' - waiting for review
2184 * 'reply' - waiting for owner to reply to review
2185 * 'not lgtm' - Code-Review -2 from at least one approved reviewer
2186 * 'lgtm' - Code-Review +2 from at least one approved reviewer
2187 * 'commit' - in the commit queue
2188 * 'closed' - abandoned
2189 """
2190 if not self.GetIssue():
2191 return None
2192
2193 try:
2194 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
2195 except httplib.HTTPException:
2196 return 'error'
2197
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002198 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002199 return 'closed'
2200
2201 cq_label = data['labels'].get('Commit-Queue', {})
2202 if cq_label:
2203 # Vote value is a stringified integer, which we expect from 0 to 2.
2204 vote_value = cq_label.get('value', '0')
2205 vote_text = cq_label.get('values', {}).get(vote_value, '')
2206 if vote_text.lower() == 'commit':
2207 return 'commit'
2208
2209 lgtm_label = data['labels'].get('Code-Review', {})
2210 if lgtm_label:
2211 if 'rejected' in lgtm_label:
2212 return 'not lgtm'
2213 if 'approved' in lgtm_label:
2214 return 'lgtm'
2215
2216 if not data.get('reviewers', {}).get('REVIEWER', []):
2217 return 'unsent'
2218
2219 messages = data.get('messages', [])
2220 if messages:
2221 owner = data['owner'].get('_account_id')
2222 last_message_author = messages[-1].get('author', {}).get('_account_id')
2223 if owner != last_message_author:
2224 # Some reply from non-owner.
2225 return 'reply'
2226
2227 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002228
2229 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002230 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002231 return data['revisions'][data['current_revision']]['_number']
2232
2233 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002234 data = self._GetChangeDetail(['CURRENT_REVISION'])
2235 current_rev = data['current_revision']
2236 url = data['revisions'][current_rev]['fetch']['http']['url']
2237 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002238
2239 def UpdateDescriptionRemote(self, description):
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002240 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
2241 description)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002242
2243 def CloseIssue(self):
2244 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2245
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002246 def GetApprovingReviewers(self):
2247 """Returns a list of reviewers approving the change.
2248
2249 Note: not necessarily committers.
2250 """
2251 raise NotImplementedError()
2252
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002253 def SubmitIssue(self, wait_for_merge=True):
2254 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2255 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002256
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002257 def _GetChangeDetail(self, options=None, issue=None):
2258 options = options or []
2259 issue = issue or self.GetIssue()
2260 assert issue, 'issue required to query Gerrit'
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002261 return gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2262 options)
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002263
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002264 def CMDLand(self, force, bypass_hooks, verbose):
2265 if git_common.is_dirty_git_tree('land'):
2266 return 1
tandriid60367b2016-06-22 05:25:12 -07002267 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2268 if u'Commit-Queue' in detail.get('labels', {}):
2269 if not force:
2270 ask_for_data('\nIt seems this repository has a Commit Queue, '
2271 'which can test and land changes for you. '
2272 'Are you sure you wish to bypass it?\n'
2273 'Press Enter to continue, Ctrl+C to abort.')
2274
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002275 differs = True
2276 last_upload = RunGit(['config',
2277 'branch.%s.gerritsquashhash' % self.GetBranch()],
2278 error_ok=True).strip()
2279 # Note: git diff outputs nothing if there is no diff.
2280 if not last_upload or RunGit(['diff', last_upload]).strip():
2281 print('WARNING: some changes from local branch haven\'t been uploaded')
2282 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002283 if detail['current_revision'] == last_upload:
2284 differs = False
2285 else:
2286 print('WARNING: local branch contents differ from latest uploaded '
2287 'patchset')
2288 if differs:
2289 if not force:
2290 ask_for_data(
2291 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2292 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2293 elif not bypass_hooks:
2294 hook_results = self.RunHook(
2295 committing=True,
2296 may_prompt=not force,
2297 verbose=verbose,
2298 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2299 if not hook_results.should_continue():
2300 return 1
2301
2302 self.SubmitIssue(wait_for_merge=True)
2303 print('Issue %s has been submitted.' % self.GetIssueURL())
2304 return 0
2305
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002306 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2307 directory):
2308 assert not reject
2309 assert not nocommit
2310 assert not directory
2311 assert parsed_issue_arg.valid
2312
2313 self._changelist.issue = parsed_issue_arg.issue
2314
2315 if parsed_issue_arg.hostname:
2316 self._gerrit_host = parsed_issue_arg.hostname
2317 self._gerrit_server = 'https://%s' % self._gerrit_host
2318
2319 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2320
2321 if not parsed_issue_arg.patchset:
2322 # Use current revision by default.
2323 revision_info = detail['revisions'][detail['current_revision']]
2324 patchset = int(revision_info['_number'])
2325 else:
2326 patchset = parsed_issue_arg.patchset
2327 for revision_info in detail['revisions'].itervalues():
2328 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2329 break
2330 else:
2331 DieWithError('Couldn\'t find patchset %i in issue %i' %
2332 (parsed_issue_arg.patchset, self.GetIssue()))
2333
2334 fetch_info = revision_info['fetch']['http']
2335 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2336 RunGit(['cherry-pick', 'FETCH_HEAD'])
2337 self.SetIssue(self.GetIssue())
2338 self.SetPatchset(patchset)
2339 print('Committed patch for issue %i pathset %i locally' %
2340 (self.GetIssue(), self.GetPatchset()))
2341 return 0
2342
2343 @staticmethod
2344 def ParseIssueURL(parsed_url):
2345 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2346 return None
2347 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2348 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2349 # Short urls like https://domain/<issue_number> can be used, but don't allow
2350 # specifying the patchset (you'd 404), but we allow that here.
2351 if parsed_url.path == '/':
2352 part = parsed_url.fragment
2353 else:
2354 part = parsed_url.path
2355 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2356 if match:
2357 return _ParsedIssueNumberArgument(
2358 issue=int(match.group(2)),
2359 patchset=int(match.group(4)) if match.group(4) else None,
2360 hostname=parsed_url.netloc)
2361 return None
2362
tandrii16e0b4e2016-06-07 10:34:28 -07002363 def _GerritCommitMsgHookCheck(self, offer_removal):
2364 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2365 if not os.path.exists(hook):
2366 return
2367 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2368 # custom developer made one.
2369 data = gclient_utils.FileRead(hook)
2370 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2371 return
2372 print('Warning: you have Gerrit commit-msg hook installed.\n'
2373 'It is not neccessary for uploading with git cl in squash mode, '
2374 'and may interfere with it in subtle ways.\n'
2375 'We recommend you remove the commit-msg hook.')
2376 if offer_removal:
2377 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2378 if reply.lower().startswith('y'):
2379 gclient_utils.rm_file_or_tree(hook)
2380 print('Gerrit commit-msg hook removed.')
2381 else:
2382 print('OK, will keep Gerrit commit-msg hook in place.')
2383
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002384 def CMDUploadChange(self, options, args, change):
2385 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002386 if options.squash and options.no_squash:
2387 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002388
2389 if not options.squash and not options.no_squash:
2390 # Load default for user, repo, squash=true, in this order.
2391 options.squash = settings.GetSquashGerritUploads()
2392 elif options.no_squash:
2393 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002394
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002395 # We assume the remote called "origin" is the one we want.
2396 # It is probably not worthwhile to support different workflows.
2397 gerrit_remote = 'origin'
2398
2399 remote, remote_branch = self.GetRemoteBranch()
2400 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2401 pending_prefix='')
2402
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002403 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002404 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002405 if self.GetIssue():
2406 # Try to get the message from a previous upload.
2407 message = self.GetDescription()
2408 if not message:
2409 DieWithError(
2410 'failed to fetch description from current Gerrit issue %d\n'
2411 '%s' % (self.GetIssue(), self.GetIssueURL()))
2412 change_id = self._GetChangeDetail()['change_id']
2413 while True:
2414 footer_change_ids = git_footers.get_footer_change_id(message)
2415 if footer_change_ids == [change_id]:
2416 break
2417 if not footer_change_ids:
2418 message = git_footers.add_footer_change_id(message, change_id)
2419 print('WARNING: appended missing Change-Id to issue description')
2420 continue
2421 # There is already a valid footer but with different or several ids.
2422 # Doing this automatically is non-trivial as we don't want to lose
2423 # existing other footers, yet we want to append just 1 desired
2424 # Change-Id. Thus, just create a new footer, but let user verify the
2425 # new description.
2426 message = '%s\n\nChange-Id: %s' % (message, change_id)
2427 print(
2428 'WARNING: issue %s has Change-Id footer(s):\n'
2429 ' %s\n'
2430 'but issue has Change-Id %s, according to Gerrit.\n'
2431 'Please, check the proposed correction to the description, '
2432 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2433 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2434 change_id))
2435 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2436 if not options.force:
2437 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002438 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002439 message = change_desc.description
2440 if not message:
2441 DieWithError("Description is empty. Aborting...")
2442 # Continue the while loop.
2443 # Sanity check of this code - we should end up with proper message
2444 # footer.
2445 assert [change_id] == git_footers.get_footer_change_id(message)
2446 change_desc = ChangeDescription(message)
2447 else:
2448 change_desc = ChangeDescription(
2449 options.message or CreateDescriptionFromLog(args))
2450 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002451 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002452 if not change_desc.description:
2453 DieWithError("Description is empty. Aborting...")
2454 message = change_desc.description
2455 change_ids = git_footers.get_footer_change_id(message)
2456 if len(change_ids) > 1:
2457 DieWithError('too many Change-Id footers, at most 1 allowed.')
2458 if not change_ids:
2459 # Generate the Change-Id automatically.
2460 message = git_footers.add_footer_change_id(
2461 message, GenerateGerritChangeId(message))
2462 change_desc.set_description(message)
2463 change_ids = git_footers.get_footer_change_id(message)
2464 assert len(change_ids) == 1
2465 change_id = change_ids[0]
2466
2467 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2468 if remote is '.':
2469 # If our upstream branch is local, we base our squashed commit on its
2470 # squashed version.
2471 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2472 # Check the squashed hash of the parent.
2473 parent = RunGit(['config',
2474 'branch.%s.gerritsquashhash' % upstream_branch_name],
2475 error_ok=True).strip()
2476 # Verify that the upstream branch has been uploaded too, otherwise
2477 # Gerrit will create additional CLs when uploading.
2478 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2479 RunGitSilent(['rev-parse', parent + ':'])):
2480 # TODO(tandrii): remove "old depot_tools" part on April 12, 2016.
2481 DieWithError(
2482 'Upload upstream branch %s first.\n'
2483 'Note: maybe you\'ve uploaded it with --no-squash or with an old '
2484 'version of depot_tools. If so, then re-upload it with:\n'
2485 ' git cl upload --squash\n' % upstream_branch_name)
2486 else:
2487 parent = self.GetCommonAncestorWithUpstream()
2488
2489 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2490 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2491 '-m', message]).strip()
2492 else:
2493 change_desc = ChangeDescription(
2494 options.message or CreateDescriptionFromLog(args))
2495 if not change_desc.description:
2496 DieWithError("Description is empty. Aborting...")
2497
2498 if not git_footers.get_footer_change_id(change_desc.description):
2499 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002500 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2501 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002502 ref_to_push = 'HEAD'
2503 parent = '%s/%s' % (gerrit_remote, branch)
2504 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2505
2506 assert change_desc
2507 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2508 ref_to_push)]).splitlines()
2509 if len(commits) > 1:
2510 print('WARNING: This will upload %d commits. Run the following command '
2511 'to see which commits will be uploaded: ' % len(commits))
2512 print('git log %s..%s' % (parent, ref_to_push))
2513 print('You can also use `git squash-branch` to squash these into a '
2514 'single commit.')
2515 ask_for_data('About to upload; enter to confirm.')
2516
2517 if options.reviewers or options.tbr_owners:
2518 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2519 change)
2520
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002521 # Extra options that can be specified at push time. Doc:
2522 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2523 refspec_opts = []
2524 if options.title:
2525 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2526 # reverse on its side.
2527 if '_' in options.title:
2528 print('WARNING: underscores in title will be converted to spaces.')
2529 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2530
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002531 if options.send_mail:
2532 if not change_desc.get_reviewers():
2533 DieWithError('Must specify reviewers to send email.')
2534 refspec_opts.append('notify=ALL')
2535 else:
2536 refspec_opts.append('notify=NONE')
2537
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002538 cc = self.GetCCList().split(',')
2539 if options.cc:
2540 cc.extend(options.cc)
2541 cc = filter(None, cc)
2542 if cc:
tandrii@chromium.org074c2af2016-06-03 23:18:40 +00002543 refspec_opts.extend('cc=' + email.strip() for email in cc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002544
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002545 if change_desc.get_reviewers():
2546 refspec_opts.extend('r=' + email.strip()
2547 for email in change_desc.get_reviewers())
2548
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002549 refspec_suffix = ''
2550 if refspec_opts:
2551 refspec_suffix = '%' + ','.join(refspec_opts)
2552 assert ' ' not in refspec_suffix, (
2553 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002554 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002555
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002556 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002557 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002558 print_stdout=True,
2559 # Flush after every line: useful for seeing progress when running as
2560 # recipe.
2561 filter_fn=lambda _: sys.stdout.flush())
2562
2563 if options.squash:
2564 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2565 change_numbers = [m.group(1)
2566 for m in map(regex.match, push_stdout.splitlines())
2567 if m]
2568 if len(change_numbers) != 1:
2569 DieWithError(
2570 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2571 'Change-Id: %s') % (len(change_numbers), change_id))
2572 self.SetIssue(change_numbers[0])
2573 RunGit(['config', 'branch.%s.gerritsquashhash' % self.GetBranch(),
2574 ref_to_push])
2575 return 0
2576
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002577 def _AddChangeIdToCommitMessage(self, options, args):
2578 """Re-commits using the current message, assumes the commit hook is in
2579 place.
2580 """
2581 log_desc = options.message or CreateDescriptionFromLog(args)
2582 git_command = ['commit', '--amend', '-m', log_desc]
2583 RunGit(git_command)
2584 new_log_desc = CreateDescriptionFromLog(args)
2585 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002586 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002587 return new_log_desc
2588 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002589 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002590
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002591 def SetCQState(self, new_state):
2592 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
2593 # TODO(tandrii): maybe allow configurability in codereview.settings or by
2594 # self-discovery of label config for this CL using REST API.
2595 vote_map = {
2596 _CQState.NONE: 0,
2597 _CQState.DRY_RUN: 1,
2598 _CQState.COMMIT : 2,
2599 }
2600 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2601 labels={'Commit-Queue': vote_map[new_state]})
2602
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002603
2604_CODEREVIEW_IMPLEMENTATIONS = {
2605 'rietveld': _RietveldChangelistImpl,
2606 'gerrit': _GerritChangelistImpl,
2607}
2608
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002609
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002610def _add_codereview_select_options(parser):
2611 """Appends --gerrit and --rietveld options to force specific codereview."""
2612 parser.codereview_group = optparse.OptionGroup(
2613 parser, 'EXPERIMENTAL! Codereview override options')
2614 parser.add_option_group(parser.codereview_group)
2615 parser.codereview_group.add_option(
2616 '--gerrit', action='store_true',
2617 help='Force the use of Gerrit for codereview')
2618 parser.codereview_group.add_option(
2619 '--rietveld', action='store_true',
2620 help='Force the use of Rietveld for codereview')
2621
2622
2623def _process_codereview_select_options(parser, options):
2624 if options.gerrit and options.rietveld:
2625 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2626 options.forced_codereview = None
2627 if options.gerrit:
2628 options.forced_codereview = 'gerrit'
2629 elif options.rietveld:
2630 options.forced_codereview = 'rietveld'
2631
2632
tandriif9aefb72016-07-01 09:06:51 -07002633def _get_bug_line_values(default_project, bugs):
2634 """Given default_project and comma separated list of bugs, yields bug line
2635 values.
2636
2637 Each bug can be either:
2638 * a number, which is combined with default_project
2639 * string, which is left as is.
2640
2641 This function may produce more than one line, because bugdroid expects one
2642 project per line.
2643
2644 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2645 ['v8:123', 'chromium:789']
2646 """
2647 default_bugs = []
2648 others = []
2649 for bug in bugs.split(','):
2650 bug = bug.strip()
2651 if bug:
2652 try:
2653 default_bugs.append(int(bug))
2654 except ValueError:
2655 others.append(bug)
2656
2657 if default_bugs:
2658 default_bugs = ','.join(map(str, default_bugs))
2659 if default_project:
2660 yield '%s:%s' % (default_project, default_bugs)
2661 else:
2662 yield default_bugs
2663 for other in sorted(others):
2664 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2665 yield other
2666
2667
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002668class ChangeDescription(object):
2669 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002670 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002671 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002672
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002673 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002674 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002675
agable@chromium.org42c20792013-09-12 17:34:49 +00002676 @property # www.logilab.org/ticket/89786
2677 def description(self): # pylint: disable=E0202
2678 return '\n'.join(self._description_lines)
2679
2680 def set_description(self, desc):
2681 if isinstance(desc, basestring):
2682 lines = desc.splitlines()
2683 else:
2684 lines = [line.rstrip() for line in desc]
2685 while lines and not lines[0]:
2686 lines.pop(0)
2687 while lines and not lines[-1]:
2688 lines.pop(-1)
2689 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002690
piman@chromium.org336f9122014-09-04 02:16:55 +00002691 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002692 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002693 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002694 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002695 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002696 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002697
agable@chromium.org42c20792013-09-12 17:34:49 +00002698 # Get the set of R= and TBR= lines and remove them from the desciption.
2699 regexp = re.compile(self.R_LINE)
2700 matches = [regexp.match(line) for line in self._description_lines]
2701 new_desc = [l for i, l in enumerate(self._description_lines)
2702 if not matches[i]]
2703 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002704
agable@chromium.org42c20792013-09-12 17:34:49 +00002705 # Construct new unified R= and TBR= lines.
2706 r_names = []
2707 tbr_names = []
2708 for match in matches:
2709 if not match:
2710 continue
2711 people = cleanup_list([match.group(2).strip()])
2712 if match.group(1) == 'TBR':
2713 tbr_names.extend(people)
2714 else:
2715 r_names.extend(people)
2716 for name in r_names:
2717 if name not in reviewers:
2718 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002719 if add_owners_tbr:
2720 owners_db = owners.Database(change.RepositoryRoot(),
2721 fopen=file, os_path=os.path, glob=glob.glob)
2722 all_reviewers = set(tbr_names + reviewers)
2723 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2724 all_reviewers)
2725 tbr_names.extend(owners_db.reviewers_for(missing_files,
2726 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002727 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2728 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2729
2730 # Put the new lines in the description where the old first R= line was.
2731 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2732 if 0 <= line_loc < len(self._description_lines):
2733 if new_tbr_line:
2734 self._description_lines.insert(line_loc, new_tbr_line)
2735 if new_r_line:
2736 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002737 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002738 if new_r_line:
2739 self.append_footer(new_r_line)
2740 if new_tbr_line:
2741 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002742
tandriif9aefb72016-07-01 09:06:51 -07002743 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002744 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002745 self.set_description([
2746 '# Enter a description of the change.',
2747 '# This will be displayed on the codereview site.',
2748 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002749 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002750 '--------------------',
2751 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002752
agable@chromium.org42c20792013-09-12 17:34:49 +00002753 regexp = re.compile(self.BUG_LINE)
2754 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07002755 prefix = settings.GetBugPrefix()
2756 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
2757 for value in values:
2758 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
2759 self.append_footer('BUG=%s' % value)
2760
agable@chromium.org42c20792013-09-12 17:34:49 +00002761 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002762 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002763 if not content:
2764 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002765 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002766
2767 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002768 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2769 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002770 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002771 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002772
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002773 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00002774 """Adds a footer line to the description.
2775
2776 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
2777 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
2778 that Gerrit footers are always at the end.
2779 """
2780 parsed_footer_line = git_footers.parse_footer(line)
2781 if parsed_footer_line:
2782 # Line is a gerrit footer in the form: Footer-Key: any value.
2783 # Thus, must be appended observing Gerrit footer rules.
2784 self.set_description(
2785 git_footers.add_footer(self.description,
2786 key=parsed_footer_line[0],
2787 value=parsed_footer_line[1]))
2788 return
2789
2790 if not self._description_lines:
2791 self._description_lines.append(line)
2792 return
2793
2794 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
2795 if gerrit_footers:
2796 # git_footers.split_footers ensures that there is an empty line before
2797 # actual (gerrit) footers, if any. We have to keep it that way.
2798 assert top_lines and top_lines[-1] == ''
2799 top_lines, separator = top_lines[:-1], top_lines[-1:]
2800 else:
2801 separator = [] # No need for separator if there are no gerrit_footers.
2802
2803 prev_line = top_lines[-1] if top_lines else ''
2804 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
2805 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2806 top_lines.append('')
2807 top_lines.append(line)
2808 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002809
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002810 def get_reviewers(self):
2811 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002812 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
2813 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002814 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002815
2816
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002817def get_approving_reviewers(props):
2818 """Retrieves the reviewers that approved a CL from the issue properties with
2819 messages.
2820
2821 Note that the list may contain reviewers that are not committer, thus are not
2822 considered by the CQ.
2823 """
2824 return sorted(
2825 set(
2826 message['sender']
2827 for message in props['messages']
2828 if message['approval'] and message['sender'] in props['reviewers']
2829 )
2830 )
2831
2832
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002833def FindCodereviewSettingsFile(filename='codereview.settings'):
2834 """Finds the given file starting in the cwd and going up.
2835
2836 Only looks up to the top of the repository unless an
2837 'inherit-review-settings-ok' file exists in the root of the repository.
2838 """
2839 inherit_ok_file = 'inherit-review-settings-ok'
2840 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002841 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002842 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2843 root = '/'
2844 while True:
2845 if filename in os.listdir(cwd):
2846 if os.path.isfile(os.path.join(cwd, filename)):
2847 return open(os.path.join(cwd, filename))
2848 if cwd == root:
2849 break
2850 cwd = os.path.dirname(cwd)
2851
2852
2853def LoadCodereviewSettingsFromFile(fileobj):
2854 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002855 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002856
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002857 def SetProperty(name, setting, unset_error_ok=False):
2858 fullname = 'rietveld.' + name
2859 if setting in keyvals:
2860 RunGit(['config', fullname, keyvals[setting]])
2861 else:
2862 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2863
2864 SetProperty('server', 'CODE_REVIEW_SERVER')
2865 # Only server setting is required. Other settings can be absent.
2866 # In that case, we ignore errors raised during option deletion attempt.
2867 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002868 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002869 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2870 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002871 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002872 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002873 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2874 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002875 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002876 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002877 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002878 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2879 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002880
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002881 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002882 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002883
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002884 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07002885 RunGit(['config', 'gerrit.squash-uploads',
2886 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002887
tandrii@chromium.org28253532016-04-14 13:46:56 +00002888 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00002889 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00002890 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
2891
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002892 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2893 #should be of the form
2894 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2895 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2896 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2897 keyvals['ORIGIN_URL_CONFIG']])
2898
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002899
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002900def urlretrieve(source, destination):
2901 """urllib is broken for SSL connections via a proxy therefore we
2902 can't use urllib.urlretrieve()."""
2903 with open(destination, 'w') as f:
2904 f.write(urllib2.urlopen(source).read())
2905
2906
ukai@chromium.org712d6102013-11-27 00:52:58 +00002907def hasSheBang(fname):
2908 """Checks fname is a #! script."""
2909 with open(fname) as f:
2910 return f.read(2).startswith('#!')
2911
2912
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00002913# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
2914def DownloadHooks(*args, **kwargs):
2915 pass
2916
2917
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002918def DownloadGerritHook(force):
2919 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002920
2921 Args:
2922 force: True to update hooks. False to install hooks if not present.
2923 """
2924 if not settings.GetIsGerrit():
2925 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00002926 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002927 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2928 if not os.access(dst, os.X_OK):
2929 if os.path.exists(dst):
2930 if not force:
2931 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002932 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002933 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002934 if not hasSheBang(dst):
2935 DieWithError('Not a script: %s\n'
2936 'You need to download from\n%s\n'
2937 'into .git/hooks/commit-msg and '
2938 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002939 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2940 except Exception:
2941 if os.path.exists(dst):
2942 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002943 DieWithError('\nFailed to download hooks.\n'
2944 'You need to download from\n%s\n'
2945 'into .git/hooks/commit-msg and '
2946 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002947
2948
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002949
2950def GetRietveldCodereviewSettingsInteractively():
2951 """Prompt the user for settings."""
2952 server = settings.GetDefaultServerUrl(error_ok=True)
2953 prompt = 'Rietveld server (host[:port])'
2954 prompt += ' [%s]' % (server or DEFAULT_SERVER)
2955 newserver = ask_for_data(prompt + ':')
2956 if not server and not newserver:
2957 newserver = DEFAULT_SERVER
2958 if newserver:
2959 newserver = gclient_utils.UpgradeToHttps(newserver)
2960 if newserver != server:
2961 RunGit(['config', 'rietveld.server', newserver])
2962
2963 def SetProperty(initial, caption, name, is_url):
2964 prompt = caption
2965 if initial:
2966 prompt += ' ("x" to clear) [%s]' % initial
2967 new_val = ask_for_data(prompt + ':')
2968 if new_val == 'x':
2969 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
2970 elif new_val:
2971 if is_url:
2972 new_val = gclient_utils.UpgradeToHttps(new_val)
2973 if new_val != initial:
2974 RunGit(['config', 'rietveld.' + name, new_val])
2975
2976 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
2977 SetProperty(settings.GetDefaultPrivateFlag(),
2978 'Private flag (rietveld only)', 'private', False)
2979 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
2980 'tree-status-url', False)
2981 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
2982 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
2983 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
2984 'run-post-upload-hook', False)
2985
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002986@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002987def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002988 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002989
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002990 print('WARNING: git cl config works for Rietveld only.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002991 'For Gerrit, see http://crbug.com/603116.')
2992 # TODO(tandrii): add Gerrit support as part of http://crbug.com/603116.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00002993 parser.add_option('--activate-update', action='store_true',
2994 help='activate auto-updating [rietveld] section in '
2995 '.git/config')
2996 parser.add_option('--deactivate-update', action='store_true',
2997 help='deactivate auto-updating [rietveld] section in '
2998 '.git/config')
2999 options, args = parser.parse_args(args)
3000
3001 if options.deactivate_update:
3002 RunGit(['config', 'rietveld.autoupdate', 'false'])
3003 return
3004
3005 if options.activate_update:
3006 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3007 return
3008
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003009 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003010 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003011 return 0
3012
3013 url = args[0]
3014 if not url.endswith('codereview.settings'):
3015 url = os.path.join(url, 'codereview.settings')
3016
3017 # Load code review settings and download hooks (if available).
3018 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3019 return 0
3020
3021
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003022def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003023 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003024 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3025 branch = ShortBranchName(branchref)
3026 _, args = parser.parse_args(args)
3027 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003028 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003029 return RunGit(['config', 'branch.%s.base-url' % branch],
3030 error_ok=False).strip()
3031 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003032 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003033 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3034 error_ok=False).strip()
3035
3036
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003037def color_for_status(status):
3038 """Maps a Changelist status to color, for CMDstatus and other tools."""
3039 return {
3040 'unsent': Fore.RED,
3041 'waiting': Fore.BLUE,
3042 'reply': Fore.YELLOW,
3043 'lgtm': Fore.GREEN,
3044 'commit': Fore.MAGENTA,
3045 'closed': Fore.CYAN,
3046 'error': Fore.WHITE,
3047 }.get(status, Fore.WHITE)
3048
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003049
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003050def get_cl_statuses(changes, fine_grained, max_processes=None):
3051 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003052
3053 If fine_grained is true, this will fetch CL statuses from the server.
3054 Otherwise, simply indicate if there's a matching url for the given branches.
3055
3056 If max_processes is specified, it is used as the maximum number of processes
3057 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3058 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003059
3060 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003061 """
3062 # Silence upload.py otherwise it becomes unwieldly.
3063 upload.verbosity = 0
3064
3065 if fine_grained:
3066 # Process one branch synchronously to work through authentication, then
3067 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003068 if changes:
3069 fetch = lambda cl: (cl, cl.GetStatus())
3070 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003071
kmarshall3bff56b2016-06-06 18:31:47 -07003072 if not changes:
3073 # Exit early if there was only one branch to fetch.
3074 return
3075
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003076 changes_to_fetch = changes[1:]
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003077 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003078 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003079 if max_processes is not None
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003080 else len(changes_to_fetch))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003081
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003082 fetched_cls = set()
3083 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003084 while True:
3085 try:
3086 row = it.next(timeout=5)
3087 except multiprocessing.TimeoutError:
3088 break
3089
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003090 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003091 yield row
3092
3093 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003094 for cl in set(changes_to_fetch) - fetched_cls:
3095 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003096
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003097 else:
3098 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003099 for cl in changes:
3100 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003101
rmistry@google.com2dd99862015-06-22 12:22:18 +00003102
3103def upload_branch_deps(cl, args):
3104 """Uploads CLs of local branches that are dependents of the current branch.
3105
3106 If the local branch dependency tree looks like:
3107 test1 -> test2.1 -> test3.1
3108 -> test3.2
3109 -> test2.2 -> test3.3
3110
3111 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3112 run on the dependent branches in this order:
3113 test2.1, test3.1, test3.2, test2.2, test3.3
3114
3115 Note: This function does not rebase your local dependent branches. Use it when
3116 you make a change to the parent branch that will not conflict with its
3117 dependent branches, and you would like their dependencies updated in
3118 Rietveld.
3119 """
3120 if git_common.is_dirty_git_tree('upload-branch-deps'):
3121 return 1
3122
3123 root_branch = cl.GetBranch()
3124 if root_branch is None:
3125 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3126 'Get on a branch!')
3127 if not cl.GetIssue() or not cl.GetPatchset():
3128 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3129 'patchset dependencies without an uploaded CL.')
3130
3131 branches = RunGit(['for-each-ref',
3132 '--format=%(refname:short) %(upstream:short)',
3133 'refs/heads'])
3134 if not branches:
3135 print('No local branches found.')
3136 return 0
3137
3138 # Create a dictionary of all local branches to the branches that are dependent
3139 # on it.
3140 tracked_to_dependents = collections.defaultdict(list)
3141 for b in branches.splitlines():
3142 tokens = b.split()
3143 if len(tokens) == 2:
3144 branch_name, tracked = tokens
3145 tracked_to_dependents[tracked].append(branch_name)
3146
vapiera7fbd5a2016-06-16 09:17:49 -07003147 print()
3148 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003149 dependents = []
3150 def traverse_dependents_preorder(branch, padding=''):
3151 dependents_to_process = tracked_to_dependents.get(branch, [])
3152 padding += ' '
3153 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003154 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003155 dependents.append(dependent)
3156 traverse_dependents_preorder(dependent, padding)
3157 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003158 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003159
3160 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003161 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003162 return 0
3163
vapiera7fbd5a2016-06-16 09:17:49 -07003164 print('This command will checkout all dependent branches and run '
3165 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003166 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3167
andybons@chromium.org962f9462016-02-03 20:00:42 +00003168 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003169 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003170 args.extend(['-t', 'Updated patchset dependency'])
3171
rmistry@google.com2dd99862015-06-22 12:22:18 +00003172 # Record all dependents that failed to upload.
3173 failures = {}
3174 # Go through all dependents, checkout the branch and upload.
3175 try:
3176 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003177 print()
3178 print('--------------------------------------')
3179 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003180 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003181 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003182 try:
3183 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003184 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003185 failures[dependent_branch] = 1
3186 except: # pylint: disable=W0702
3187 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003188 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003189 finally:
3190 # Swap back to the original root branch.
3191 RunGit(['checkout', '-q', root_branch])
3192
vapiera7fbd5a2016-06-16 09:17:49 -07003193 print()
3194 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003195 for dependent_branch in dependents:
3196 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003197 print(' %s : %s' % (dependent_branch, upload_status))
3198 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003199
3200 return 0
3201
3202
kmarshall3bff56b2016-06-06 18:31:47 -07003203def CMDarchive(parser, args):
3204 """Archives and deletes branches associated with closed changelists."""
3205 parser.add_option(
3206 '-j', '--maxjobs', action='store', type=int,
3207 help='The maximum number of jobs to use when retrieving review status')
3208 parser.add_option(
3209 '-f', '--force', action='store_true',
3210 help='Bypasses the confirmation prompt.')
3211
3212 auth.add_auth_options(parser)
3213 options, args = parser.parse_args(args)
3214 if args:
3215 parser.error('Unsupported args: %s' % ' '.join(args))
3216 auth_config = auth.extract_auth_config_from_options(options)
3217
3218 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3219 if not branches:
3220 return 0
3221
vapiera7fbd5a2016-06-16 09:17:49 -07003222 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003223 changes = [Changelist(branchref=b, auth_config=auth_config)
3224 for b in branches.splitlines()]
3225 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3226 statuses = get_cl_statuses(changes,
3227 fine_grained=True,
3228 max_processes=options.maxjobs)
3229 proposal = [(cl.GetBranch(),
3230 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3231 for cl, status in statuses
3232 if status == 'closed']
3233 proposal.sort()
3234
3235 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003236 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003237 return 0
3238
3239 current_branch = GetCurrentBranch()
3240
vapiera7fbd5a2016-06-16 09:17:49 -07003241 print('\nBranches with closed issues that will be archived:\n')
3242 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
kmarshall3bff56b2016-06-06 18:31:47 -07003243 for next_item in proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003244 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003245
3246 if any(branch == current_branch for branch, _ in proposal):
3247 print('You are currently on a branch \'%s\' which is associated with a '
3248 'closed codereview issue, so archive cannot proceed. Please '
3249 'checkout another branch and run this command again.' %
3250 current_branch)
3251 return 1
3252
3253 if not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003254 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3255 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003256 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003257 return 1
3258
3259 for branch, tagname in proposal:
3260 RunGit(['tag', tagname, branch])
3261 RunGit(['branch', '-D', branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003262 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003263
3264 return 0
3265
3266
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003267def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003268 """Show status of changelists.
3269
3270 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003271 - Red not sent for review or broken
3272 - Blue waiting for review
3273 - Yellow waiting for you to reply to review
3274 - Green LGTM'ed
3275 - Magenta in the commit queue
3276 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003277
3278 Also see 'git cl comments'.
3279 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003280 parser.add_option('--field',
3281 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003282 parser.add_option('-f', '--fast', action='store_true',
3283 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003284 parser.add_option(
3285 '-j', '--maxjobs', action='store', type=int,
3286 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003287
3288 auth.add_auth_options(parser)
3289 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003290 if args:
3291 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003292 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003293
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003294 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003295 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003296 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003297 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003298 elif options.field == 'id':
3299 issueid = cl.GetIssue()
3300 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003301 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003302 elif options.field == 'patch':
3303 patchset = cl.GetPatchset()
3304 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003305 print(patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003306 elif options.field == 'url':
3307 url = cl.GetIssueURL()
3308 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003309 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003310 return 0
3311
3312 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3313 if not branches:
3314 print('No local branch found.')
3315 return 0
3316
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003317 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003318 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003319 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003320 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003321 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003322 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003323 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003324
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003325 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003326 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3327 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3328 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003329 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003330 c, status = output.next()
3331 branch_statuses[c.GetBranch()] = status
3332 status = branch_statuses.pop(branch)
3333 url = cl.GetIssueURL()
3334 if url and (not status or status == 'error'):
3335 # The issue probably doesn't exist anymore.
3336 url += ' (broken)'
3337
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003338 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003339 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003340 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003341 color = ''
3342 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003343 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003344 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003345 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003346 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003347
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003348 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003349 print()
3350 print('Current branch:',)
3351 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003352 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003353 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003354 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003355 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003356 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003357 print('Issue description:')
3358 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003359 return 0
3360
3361
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003362def colorize_CMDstatus_doc():
3363 """To be called once in main() to add colors to git cl status help."""
3364 colors = [i for i in dir(Fore) if i[0].isupper()]
3365
3366 def colorize_line(line):
3367 for color in colors:
3368 if color in line.upper():
3369 # Extract whitespaces first and the leading '-'.
3370 indent = len(line) - len(line.lstrip(' ')) + 1
3371 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3372 return line
3373
3374 lines = CMDstatus.__doc__.splitlines()
3375 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3376
3377
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003378@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003379def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003380 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003381
3382 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003383 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003384 parser.add_option('-r', '--reverse', action='store_true',
3385 help='Lookup the branch(es) for the specified issues. If '
3386 'no issues are specified, all branches with mapped '
3387 'issues will be listed.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003388 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003389 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003390 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003391
dnj@chromium.org406c4402015-03-03 17:22:28 +00003392 if options.reverse:
3393 branches = RunGit(['for-each-ref', 'refs/heads',
3394 '--format=%(refname:short)']).splitlines()
3395
3396 # Reverse issue lookup.
3397 issue_branch_map = {}
3398 for branch in branches:
3399 cl = Changelist(branchref=branch)
3400 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3401 if not args:
3402 args = sorted(issue_branch_map.iterkeys())
3403 for issue in args:
3404 if not issue:
3405 continue
vapiera7fbd5a2016-06-16 09:17:49 -07003406 print('Branch for issue number %s: %s' % (
3407 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
dnj@chromium.org406c4402015-03-03 17:22:28 +00003408 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003409 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003410 if len(args) > 0:
3411 try:
3412 issue = int(args[0])
3413 except ValueError:
3414 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003415 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003416 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003417 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003418 return 0
3419
3420
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003421def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003422 """Shows or posts review comments for any changelist."""
3423 parser.add_option('-a', '--add-comment', dest='comment',
3424 help='comment to add to an issue')
3425 parser.add_option('-i', dest='issue',
3426 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003427 parser.add_option('-j', '--json-file',
3428 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003429 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003430 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003431 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003432
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003433 issue = None
3434 if options.issue:
3435 try:
3436 issue = int(options.issue)
3437 except ValueError:
3438 DieWithError('A review issue id is expected to be a number')
3439
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003440 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003441
3442 if options.comment:
3443 cl.AddComment(options.comment)
3444 return 0
3445
3446 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003447 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003448 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003449 summary.append({
3450 'date': message['date'],
3451 'lgtm': False,
3452 'message': message['text'],
3453 'not_lgtm': False,
3454 'sender': message['sender'],
3455 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003456 if message['disapproval']:
3457 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003458 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003459 elif message['approval']:
3460 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003461 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003462 elif message['sender'] == data['owner_email']:
3463 color = Fore.MAGENTA
3464 else:
3465 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003466 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003467 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003468 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003469 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003470 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003471 if options.json_file:
3472 with open(options.json_file, 'wb') as f:
3473 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003474 return 0
3475
3476
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003477@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003478def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003479 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003480 parser.add_option('-d', '--display', action='store_true',
3481 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003482 parser.add_option('-n', '--new-description',
3483 help='New description to set for this issue (- for stdin)')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003484
3485 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003486 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003487 options, args = parser.parse_args(args)
3488 _process_codereview_select_options(parser, options)
3489
3490 target_issue = None
3491 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07003492 target_issue = ParseIssueNumberArgument(args[0])
3493 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003494 parser.print_help()
3495 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003496
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003497 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003498
martiniss6eda05f2016-06-30 10:18:35 -07003499 kwargs = {
3500 'auth_config': auth_config,
3501 'codereview': options.forced_codereview,
3502 }
3503 if target_issue:
3504 kwargs['issue'] = target_issue.issue
3505 if options.forced_codereview == 'rietveld':
3506 kwargs['rietveld_server'] = target_issue.hostname
3507
3508 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003509
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003510 if not cl.GetIssue():
3511 DieWithError('This branch has no associated changelist.')
3512 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003513
smut@google.com34fb6b12015-07-13 20:03:26 +00003514 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003515 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003516 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003517
3518 if options.new_description:
3519 text = options.new_description
3520 if text == '-':
3521 text = '\n'.join(l.rstrip() for l in sys.stdin)
3522
3523 description.set_description(text)
3524 else:
3525 description.prompt()
3526
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003527 if cl.GetDescription() != description.description:
3528 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003529 return 0
3530
3531
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003532def CreateDescriptionFromLog(args):
3533 """Pulls out the commit log to use as a base for the CL description."""
3534 log_args = []
3535 if len(args) == 1 and not args[0].endswith('.'):
3536 log_args = [args[0] + '..']
3537 elif len(args) == 1 and args[0].endswith('...'):
3538 log_args = [args[0][:-1]]
3539 elif len(args) == 2:
3540 log_args = [args[0] + '..' + args[1]]
3541 else:
3542 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003543 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003544
3545
thestig@chromium.org44202a22014-03-11 19:22:18 +00003546def CMDlint(parser, args):
3547 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003548 parser.add_option('--filter', action='append', metavar='-x,+y',
3549 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003550 auth.add_auth_options(parser)
3551 options, args = parser.parse_args(args)
3552 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003553
3554 # Access to a protected member _XX of a client class
3555 # pylint: disable=W0212
3556 try:
3557 import cpplint
3558 import cpplint_chromium
3559 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003560 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003561 return 1
3562
3563 # Change the current working directory before calling lint so that it
3564 # shows the correct base.
3565 previous_cwd = os.getcwd()
3566 os.chdir(settings.GetRoot())
3567 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003568 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003569 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3570 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003571 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003572 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003573 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003574
3575 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003576 command = args + files
3577 if options.filter:
3578 command = ['--filter=' + ','.join(options.filter)] + command
3579 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003580
3581 white_regex = re.compile(settings.GetLintRegex())
3582 black_regex = re.compile(settings.GetLintIgnoreRegex())
3583 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3584 for filename in filenames:
3585 if white_regex.match(filename):
3586 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07003587 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003588 else:
3589 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3590 extra_check_functions)
3591 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003592 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003593 finally:
3594 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07003595 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003596 if cpplint._cpplint_state.error_count != 0:
3597 return 1
3598 return 0
3599
3600
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003601def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003602 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003603 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003604 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003605 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003606 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003607 auth.add_auth_options(parser)
3608 options, args = parser.parse_args(args)
3609 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003610
sbc@chromium.org71437c02015-04-09 19:29:40 +00003611 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07003612 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003613 return 1
3614
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003615 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003616 if args:
3617 base_branch = args[0]
3618 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003619 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003620 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003621
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003622 cl.RunHook(
3623 committing=not options.upload,
3624 may_prompt=False,
3625 verbose=options.verbose,
3626 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003627 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003628
3629
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003630def GenerateGerritChangeId(message):
3631 """Returns Ixxxxxx...xxx change id.
3632
3633 Works the same way as
3634 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3635 but can be called on demand on all platforms.
3636
3637 The basic idea is to generate git hash of a state of the tree, original commit
3638 message, author/committer info and timestamps.
3639 """
3640 lines = []
3641 tree_hash = RunGitSilent(['write-tree'])
3642 lines.append('tree %s' % tree_hash.strip())
3643 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3644 if code == 0:
3645 lines.append('parent %s' % parent.strip())
3646 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3647 lines.append('author %s' % author.strip())
3648 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3649 lines.append('committer %s' % committer.strip())
3650 lines.append('')
3651 # Note: Gerrit's commit-hook actually cleans message of some lines and
3652 # whitespace. This code is not doing this, but it clearly won't decrease
3653 # entropy.
3654 lines.append(message)
3655 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3656 stdin='\n'.join(lines))
3657 return 'I%s' % change_hash.strip()
3658
3659
wittman@chromium.org455dc922015-01-26 20:15:50 +00003660def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3661 """Computes the remote branch ref to use for the CL.
3662
3663 Args:
3664 remote (str): The git remote for the CL.
3665 remote_branch (str): The git remote branch for the CL.
3666 target_branch (str): The target branch specified by the user.
3667 pending_prefix (str): The pending prefix from the settings.
3668 """
3669 if not (remote and remote_branch):
3670 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003671
wittman@chromium.org455dc922015-01-26 20:15:50 +00003672 if target_branch:
3673 # Cannonicalize branch references to the equivalent local full symbolic
3674 # refs, which are then translated into the remote full symbolic refs
3675 # below.
3676 if '/' not in target_branch:
3677 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3678 else:
3679 prefix_replacements = (
3680 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3681 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3682 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3683 )
3684 match = None
3685 for regex, replacement in prefix_replacements:
3686 match = re.search(regex, target_branch)
3687 if match:
3688 remote_branch = target_branch.replace(match.group(0), replacement)
3689 break
3690 if not match:
3691 # This is a branch path but not one we recognize; use as-is.
3692 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003693 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3694 # Handle the refs that need to land in different refs.
3695 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003696
wittman@chromium.org455dc922015-01-26 20:15:50 +00003697 # Create the true path to the remote branch.
3698 # Does the following translation:
3699 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3700 # * refs/remotes/origin/master -> refs/heads/master
3701 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3702 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3703 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3704 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3705 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3706 'refs/heads/')
3707 elif remote_branch.startswith('refs/remotes/branch-heads'):
3708 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3709 # If a pending prefix exists then replace refs/ with it.
3710 if pending_prefix:
3711 remote_branch = remote_branch.replace('refs/', pending_prefix)
3712 return remote_branch
3713
3714
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003715def cleanup_list(l):
3716 """Fixes a list so that comma separated items are put as individual items.
3717
3718 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3719 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3720 """
3721 items = sum((i.split(',') for i in l), [])
3722 stripped_items = (i.strip() for i in items)
3723 return sorted(filter(None, stripped_items))
3724
3725
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003726@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003727def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003728 """Uploads the current changelist to codereview.
3729
3730 Can skip dependency patchset uploads for a branch by running:
3731 git config branch.branch_name.skip-deps-uploads True
3732 To unset run:
3733 git config --unset branch.branch_name.skip-deps-uploads
3734 Can also set the above globally by using the --global flag.
3735 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003736 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3737 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003738 parser.add_option('--bypass-watchlists', action='store_true',
3739 dest='bypass_watchlists',
3740 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003741 parser.add_option('-f', action='store_true', dest='force',
3742 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003743 parser.add_option('-m', dest='message', help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07003744 parser.add_option('-b', '--bug',
3745 help='pre-populate the bug number(s) for this issue. '
3746 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07003747 parser.add_option('--message-file', dest='message_file',
3748 help='file which contains message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003749 parser.add_option('-t', dest='title',
3750 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003751 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003752 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003753 help='reviewer email addresses')
3754 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003755 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003756 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003757 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003758 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003759 parser.add_option('--emulate_svn_auto_props',
3760 '--emulate-svn-auto-props',
3761 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003762 dest="emulate_svn_auto_props",
3763 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003764 parser.add_option('-c', '--use-commit-queue', action='store_true',
3765 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003766 parser.add_option('--private', action='store_true',
3767 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003768 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003769 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003770 metavar='TARGET',
3771 help='Apply CL to remote ref TARGET. ' +
3772 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003773 parser.add_option('--squash', action='store_true',
3774 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003775 parser.add_option('--no-squash', action='store_true',
3776 help='Don\'t squash multiple commits into one ' +
3777 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003778 parser.add_option('--email', default=None,
3779 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003780 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3781 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003782 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3783 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003784 help='Send the patchset to do a CQ dry run right after '
3785 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003786 parser.add_option('--dependencies', action='store_true',
3787 help='Uploads CLs of all the local branches that depend on '
3788 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003789
rmistry@google.com2dd99862015-06-22 12:22:18 +00003790 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003791 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003792 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003793 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003794 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003795 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003796 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003797
sbc@chromium.org71437c02015-04-09 19:29:40 +00003798 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003799 return 1
3800
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003801 options.reviewers = cleanup_list(options.reviewers)
3802 options.cc = cleanup_list(options.cc)
3803
tandriib80458a2016-06-23 12:20:07 -07003804 if options.message_file:
3805 if options.message:
3806 parser.error('only one of --message and --message-file allowed.')
3807 options.message = gclient_utils.FileRead(options.message_file)
3808 options.message_file = None
3809
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003810 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3811 settings.GetIsGerrit()
3812
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003813 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00003814 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003815
3816
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003817def IsSubmoduleMergeCommit(ref):
3818 # When submodules are added to the repo, we expect there to be a single
3819 # non-git-svn merge commit at remote HEAD with a signature comment.
3820 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003821 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003822 return RunGit(cmd) != ''
3823
3824
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003825def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003826 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003827
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003828 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3829 upstream and closes the issue automatically and atomically.
3830
3831 Otherwise (in case of Rietveld):
3832 Squashes branch into a single commit.
3833 Updates changelog with metadata (e.g. pointer to review).
3834 Pushes/dcommits the code upstream.
3835 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003836 """
3837 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3838 help='bypass upload presubmit hook')
3839 parser.add_option('-m', dest='message',
3840 help="override review description")
3841 parser.add_option('-f', action='store_true', dest='force',
3842 help="force yes to questions (don't prompt)")
3843 parser.add_option('-c', dest='contributor',
3844 help="external contributor for patch (appended to " +
3845 "description and used as author for git). Should be " +
3846 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003847 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003848 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003849 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003850 auth_config = auth.extract_auth_config_from_options(options)
3851
3852 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003853
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003854 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3855 if cl.IsGerrit():
3856 if options.message:
3857 # This could be implemented, but it requires sending a new patch to
3858 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3859 # Besides, Gerrit has the ability to change the commit message on submit
3860 # automatically, thus there is no need to support this option (so far?).
3861 parser.error('-m MESSAGE option is not supported for Gerrit.')
3862 if options.contributor:
3863 parser.error(
3864 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3865 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3866 'the contributor\'s "name <email>". If you can\'t upload such a '
3867 'commit for review, contact your repository admin and request'
3868 '"Forge-Author" permission.')
3869 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3870 options.verbose)
3871
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003872 current = cl.GetBranch()
3873 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3874 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07003875 print()
3876 print('Attempting to push branch %r into another local branch!' % current)
3877 print()
3878 print('Either reparent this branch on top of origin/master:')
3879 print(' git reparent-branch --root')
3880 print()
3881 print('OR run `git rebase-update` if you think the parent branch is ')
3882 print('already committed.')
3883 print()
3884 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003885 return 1
3886
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003887 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003888 # Default to merging against our best guess of the upstream branch.
3889 args = [cl.GetUpstreamBranch()]
3890
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003891 if options.contributor:
3892 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07003893 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003894 return 1
3895
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003896 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003897 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003898
sbc@chromium.org71437c02015-04-09 19:29:40 +00003899 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003900 return 1
3901
3902 # This rev-list syntax means "show all commits not in my branch that
3903 # are in base_branch".
3904 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3905 base_branch]).splitlines()
3906 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07003907 print('Base branch "%s" has %d commits '
3908 'not in this branch.' % (base_branch, len(upstream_commits)))
3909 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003910 return 1
3911
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003912 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003913 svn_head = None
3914 if cmd == 'dcommit' or base_has_submodules:
3915 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
3916 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003917
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003918 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003919 # If the base_head is a submodule merge commit, the first parent of the
3920 # base_head should be a git-svn commit, which is what we're interested in.
3921 base_svn_head = base_branch
3922 if base_has_submodules:
3923 base_svn_head += '^1'
3924
3925 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003926 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07003927 print('This branch has %d additional commits not upstreamed yet.'
3928 % len(extra_commits.splitlines()))
3929 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
3930 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003931 return 1
3932
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003933 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003934 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003935 author = None
3936 if options.contributor:
3937 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003938 hook_results = cl.RunHook(
3939 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003940 may_prompt=not options.force,
3941 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003942 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003943 if not hook_results.should_continue():
3944 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003945
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003946 # Check the tree status if the tree status URL is set.
3947 status = GetTreeStatus()
3948 if 'closed' == status:
3949 print('The tree is closed. Please wait for it to reopen. Use '
3950 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3951 return 1
3952 elif 'unknown' == status:
3953 print('Unable to determine tree status. Please verify manually and '
3954 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3955 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003956
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003957 change_desc = ChangeDescription(options.message)
3958 if not change_desc.description and cl.GetIssue():
3959 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003960
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003961 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00003962 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003963 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00003964 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003965 print('No description set.')
3966 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00003967 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003968
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003969 # Keep a separate copy for the commit message, because the commit message
3970 # contains the link to the Rietveld issue, while the Rietveld message contains
3971 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003972 # Keep a separate copy for the commit message.
3973 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00003974 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003975
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003976 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00003977 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00003978 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00003979 # after it. Add a period on a new line to circumvent this. Also add a space
3980 # before the period to make sure that Gitiles continues to correctly resolve
3981 # the URL.
3982 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003983 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003984 commit_desc.append_footer('Patch from %s.' % options.contributor)
3985
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00003986 print('Description:')
3987 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003988
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003989 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003990 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00003991 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003992
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003993 # We want to squash all this branch's commits into one commit with the proper
3994 # description. We do this by doing a "reset --soft" to the base branch (which
3995 # keeps the working copy the same), then dcommitting that. If origin/master
3996 # has a submodule merge commit, we'll also need to cherry-pick the squashed
3997 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003998 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003999 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4000 # Delete the branches if they exist.
4001 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4002 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4003 result = RunGitWithCode(showref_cmd)
4004 if result[0] == 0:
4005 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004006
4007 # We might be in a directory that's present in this branch but not in the
4008 # trunk. Move up to the top of the tree so that git commands that expect a
4009 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004010 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004011 if rel_base_path:
4012 os.chdir(rel_base_path)
4013
4014 # Stuff our change into the merge branch.
4015 # We wrap in a try...finally block so if anything goes wrong,
4016 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004017 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004018 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004019 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004020 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004021 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00004022 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004023 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004024 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004025 RunGit(
4026 [
4027 'commit', '--author', options.contributor,
4028 '-m', commit_desc.description,
4029 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004030 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004031 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004032 if base_has_submodules:
4033 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
4034 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
4035 RunGit(['checkout', CHERRY_PICK_BRANCH])
4036 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004037 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004038 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004039 mirror = settings.GetGitMirror(remote)
4040 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004041 pending_prefix = settings.GetPendingRefPrefix()
4042 if not pending_prefix or branch.startswith(pending_prefix):
4043 # If not using refs/pending/heads/* at all, or target ref is already set
4044 # to pending, then push to the target ref directly.
4045 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004046 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004047 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004048 else:
4049 # Cherry-pick the change on top of pending ref and then push it.
4050 assert branch.startswith('refs/'), branch
4051 assert pending_prefix[-1] == '/', pending_prefix
4052 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004053 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004054 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004055 if retcode == 0:
4056 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004057 else:
4058 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004059 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00004060 'svn', 'dcommit',
4061 '-C%s' % options.similarity,
4062 '--no-rebase', '--rmdir',
4063 ]
4064 if settings.GetForceHttpsCommitUrl():
4065 # Allow forcing https commit URLs for some projects that don't allow
4066 # committing to http URLs (like Google Code).
4067 remote_url = cl.GetGitSvnRemoteUrl()
4068 if urlparse.urlparse(remote_url).scheme == 'http':
4069 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004070 cmd_args.append('--commit-url=%s' % remote_url)
4071 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004072 if 'Committed r' in output:
4073 revision = re.match(
4074 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4075 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004076 finally:
4077 # And then swap back to the original branch and clean up.
4078 RunGit(['checkout', '-q', cl.GetBranch()])
4079 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004080 if base_has_submodules:
4081 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004082
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004083 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004084 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004085 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004086
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004087 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004088 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004089 try:
4090 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4091 # We set pushed_to_pending to False, since it made it all the way to the
4092 # real ref.
4093 pushed_to_pending = False
4094 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004095 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004096
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004097 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004098 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004099 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004100 if not to_pending:
4101 if viewvc_url and revision:
4102 change_desc.append_footer(
4103 'Committed: %s%s' % (viewvc_url, revision))
4104 elif revision:
4105 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004106 print('Closing issue '
4107 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004108 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004109 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004110 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004111 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004112 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004113 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004114 if options.bypass_hooks:
4115 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4116 else:
4117 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004118 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004119
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004120 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004121 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004122 print('The commit is in the pending queue (%s).' % pending_ref)
4123 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4124 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004125
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004126 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4127 if os.path.isfile(hook):
4128 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004129
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004130 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004131
4132
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004133def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004134 print()
4135 print('Waiting for commit to be landed on %s...' % real_ref)
4136 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004137 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4138 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004139 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004140
4141 loop = 0
4142 while True:
4143 sys.stdout.write('fetching (%d)... \r' % loop)
4144 sys.stdout.flush()
4145 loop += 1
4146
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004147 if mirror:
4148 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004149 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4150 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4151 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4152 for commit in commits.splitlines():
4153 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004154 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004155 return commit
4156
4157 current_rev = to_rev
4158
4159
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004160def PushToGitPending(remote, pending_ref, upstream_ref):
4161 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4162
4163 Returns:
4164 (retcode of last operation, output log of last operation).
4165 """
4166 assert pending_ref.startswith('refs/'), pending_ref
4167 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4168 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4169 code = 0
4170 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004171 max_attempts = 3
4172 attempts_left = max_attempts
4173 while attempts_left:
4174 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004175 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004176 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004177
4178 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004179 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004180 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004181 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004182 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004183 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004184 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004185 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004186 continue
4187
4188 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004189 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004190 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004191 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004192 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004193 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4194 'the following files have merge conflicts:' % pending_ref)
4195 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4196 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004197 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004198 return code, out
4199
4200 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004201 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004202 code, out = RunGitWithCode(
4203 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4204 if code == 0:
4205 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004206 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004207 return code, out
4208
vapiera7fbd5a2016-06-16 09:17:49 -07004209 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004210 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004211 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004212 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004213 print('Fatal push error. Make sure your .netrc credentials and git '
4214 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004215 return code, out
4216
vapiera7fbd5a2016-06-16 09:17:49 -07004217 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004218 return code, out
4219
4220
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004221def IsFatalPushFailure(push_stdout):
4222 """True if retrying push won't help."""
4223 return '(prohibited by Gerrit)' in push_stdout
4224
4225
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004226@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004227def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004228 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004229 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004230 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004231 # If it looks like previous commits were mirrored with git-svn.
4232 message = """This repository appears to be a git-svn mirror, but no
4233upstream SVN master is set. You probably need to run 'git auto-svn' once."""
4234 else:
4235 message = """This doesn't appear to be an SVN repository.
4236If your project has a true, writeable git repository, you probably want to run
4237'git cl land' instead.
4238If your project has a git mirror of an upstream SVN master, you probably need
4239to run 'git svn init'.
4240
4241Using the wrong command might cause your commit to appear to succeed, and the
4242review to be closed, without actually landing upstream. If you choose to
4243proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004244 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004245 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
tandrii3bb82ff2016-06-17 07:36:36 -07004246 # TODO(tandrii): kill this post SVN migration with
4247 # https://codereview.chromium.org/2076683002
4248 print('WARNING: chrome infrastructure is migrating SVN repos to Git.\n'
4249 'Please let us know of this project you are committing to:'
4250 ' http://crbug.com/600451')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004251 return SendUpstream(parser, args, 'dcommit')
4252
4253
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004254@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004255def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004256 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004257 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004258 print('This appears to be an SVN repository.')
4259 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004260 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004261 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004262 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004263
4264
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004265@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004266def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004267 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004268 parser.add_option('-b', dest='newbranch',
4269 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004270 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004271 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004272 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4273 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004274 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004275 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004276 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004277 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004278 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004279 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004280
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004281
4282 group = optparse.OptionGroup(
4283 parser,
4284 'Options for continuing work on the current issue uploaded from a '
4285 'different clone (e.g. different machine). Must be used independently '
4286 'from the other options. No issue number should be specified, and the '
4287 'branch must have an issue number associated with it')
4288 group.add_option('--reapply', action='store_true', dest='reapply',
4289 help='Reset the branch and reapply the issue.\n'
4290 'CAUTION: This will undo any local changes in this '
4291 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004292
4293 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004294 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004295 parser.add_option_group(group)
4296
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004297 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004298 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004299 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004300 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004301 auth_config = auth.extract_auth_config_from_options(options)
4302
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004303
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004304 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004305 if options.newbranch:
4306 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004307 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004308 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004309
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004310 cl = Changelist(auth_config=auth_config,
4311 codereview=options.forced_codereview)
4312 if not cl.GetIssue():
4313 parser.error('current branch must have an associated issue')
4314
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004315 upstream = cl.GetUpstreamBranch()
4316 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004317 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004318
4319 RunGit(['reset', '--hard', upstream])
4320 if options.pull:
4321 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004322
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004323 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4324 options.directory)
4325
4326 if len(args) != 1 or not args[0]:
4327 parser.error('Must specify issue number or url')
4328
4329 # We don't want uncommitted changes mixed up with the patch.
4330 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004331 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004332
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004333 if options.newbranch:
4334 if options.force:
4335 RunGit(['branch', '-D', options.newbranch],
4336 stderr=subprocess2.PIPE, error_ok=True)
4337 RunGit(['new-branch', options.newbranch])
4338
4339 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4340
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004341 if cl.IsGerrit():
4342 if options.reject:
4343 parser.error('--reject is not supported with Gerrit codereview.')
4344 if options.nocommit:
4345 parser.error('--nocommit is not supported with Gerrit codereview.')
4346 if options.directory:
4347 parser.error('--directory is not supported with Gerrit codereview.')
4348
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004349 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004350 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004351
4352
4353def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004354 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004355 # Provide a wrapper for git svn rebase to help avoid accidental
4356 # git svn dcommit.
4357 # It's the only command that doesn't use parser at all since we just defer
4358 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004359
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004360 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004361
4362
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004363def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004364 """Fetches the tree status and returns either 'open', 'closed',
4365 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004366 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004367 if url:
4368 status = urllib2.urlopen(url).read().lower()
4369 if status.find('closed') != -1 or status == '0':
4370 return 'closed'
4371 elif status.find('open') != -1 or status == '1':
4372 return 'open'
4373 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004374 return 'unset'
4375
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004376
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004377def GetTreeStatusReason():
4378 """Fetches the tree status from a json url and returns the message
4379 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004380 url = settings.GetTreeStatusUrl()
4381 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004382 connection = urllib2.urlopen(json_url)
4383 status = json.loads(connection.read())
4384 connection.close()
4385 return status['message']
4386
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004387
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004388def GetBuilderMaster(bot_list):
4389 """For a given builder, fetch the master from AE if available."""
4390 map_url = 'https://builders-map.appspot.com/'
4391 try:
4392 master_map = json.load(urllib2.urlopen(map_url))
4393 except urllib2.URLError as e:
4394 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4395 (map_url, e))
4396 except ValueError as e:
4397 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4398 if not master_map:
4399 return None, 'Failed to build master map.'
4400
4401 result_master = ''
4402 for bot in bot_list:
4403 builder = bot.split(':', 1)[0]
4404 master_list = master_map.get(builder, [])
4405 if not master_list:
4406 return None, ('No matching master for builder %s.' % builder)
4407 elif len(master_list) > 1:
4408 return None, ('The builder name %s exists in multiple masters %s.' %
4409 (builder, master_list))
4410 else:
4411 cur_master = master_list[0]
4412 if not result_master:
4413 result_master = cur_master
4414 elif result_master != cur_master:
4415 return None, 'The builders do not belong to the same master.'
4416 return result_master, None
4417
4418
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004419def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004420 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004421 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004422 status = GetTreeStatus()
4423 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004424 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004425 return 2
4426
vapiera7fbd5a2016-06-16 09:17:49 -07004427 print('The tree is %s' % status)
4428 print()
4429 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004430 if status != 'open':
4431 return 1
4432 return 0
4433
4434
maruel@chromium.org15192402012-09-06 12:38:29 +00004435def CMDtry(parser, args):
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004436 """Triggers try jobs through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00004437 group = optparse.OptionGroup(parser, "Try job options")
4438 group.add_option(
4439 "-b", "--bot", action="append",
4440 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
4441 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004442 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00004443 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004444 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00004445 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004446 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00004447 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004448 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004449 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004450 "-r", "--revision",
4451 help="Revision to use for the try job; default: the "
4452 "revision will be determined by the try server; see "
4453 "its waterfall for more info")
4454 group.add_option(
4455 "-c", "--clobber", action="store_true", default=False,
4456 help="Force a clobber before building; e.g. don't do an "
4457 "incremental build")
4458 group.add_option(
4459 "--project",
4460 help="Override which project to use. Projects are defined "
4461 "server-side to define what default bot set to use")
4462 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004463 "-p", "--property", dest="properties", action="append", default=[],
4464 help="Specify generic properties in the form -p key1=value1 -p "
4465 "key2=value2 etc (buildbucket only). The value will be treated as "
4466 "json if decodable, or as string otherwise.")
4467 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004468 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004469 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004470 "--use-rietveld", action="store_true", default=False,
4471 help="Use Rietveld to trigger try jobs.")
4472 group.add_option(
4473 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4474 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004475 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004476 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004477 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004478 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004479
machenbach@chromium.org45453142015-09-15 08:45:22 +00004480 if options.use_rietveld and options.properties:
4481 parser.error('Properties can only be specified with buildbucket')
4482
4483 # Make sure that all properties are prop=value pairs.
4484 bad_params = [x for x in options.properties if '=' not in x]
4485 if bad_params:
4486 parser.error('Got properties with missing "=": %s' % bad_params)
4487
maruel@chromium.org15192402012-09-06 12:38:29 +00004488 if args:
4489 parser.error('Unknown arguments: %s' % args)
4490
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004491 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004492 if not cl.GetIssue():
4493 parser.error('Need to upload first')
4494
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004495 if cl.IsGerrit():
4496 parser.error(
4497 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4498 'If your project has Commit Queue, dry run is a workaround:\n'
4499 ' git cl set-commit --dry-run')
4500 # Code below assumes Rietveld issue.
4501 # TODO(tandrii): actually implement for Gerrit http://crbug.com/599931.
4502
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004503 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004504 if props.get('closed'):
4505 parser.error('Cannot send tryjobs for a closed CL')
4506
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004507 if props.get('private'):
4508 parser.error('Cannot use trybots with private issue')
4509
maruel@chromium.org15192402012-09-06 12:38:29 +00004510 if not options.name:
4511 options.name = cl.GetBranch()
4512
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004513 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004514 options.master, err_msg = GetBuilderMaster(options.bot)
4515 if err_msg:
4516 parser.error('Tryserver master cannot be found because: %s\n'
4517 'Please manually specify the tryserver master'
4518 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004519
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004520 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004521 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004522 if not options.bot:
4523 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004524
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004525 # Get try masters from PRESUBMIT.py files.
4526 masters = presubmit_support.DoGetTryMasters(
4527 change,
4528 change.LocalPaths(),
4529 settings.GetRoot(),
4530 None,
4531 None,
4532 options.verbose,
4533 sys.stdout)
4534 if masters:
4535 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004536
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004537 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4538 options.bot = presubmit_support.DoGetTrySlaves(
4539 change,
4540 change.LocalPaths(),
4541 settings.GetRoot(),
4542 None,
4543 None,
4544 options.verbose,
4545 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004546
4547 if not options.bot:
4548 # Get try masters from cq.cfg if any.
4549 # TODO(tandrii): some (but very few) projects store cq.cfg in different
4550 # location.
4551 cq_cfg = os.path.join(change.RepositoryRoot(),
4552 'infra', 'config', 'cq.cfg')
4553 if os.path.exists(cq_cfg):
4554 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00004555 cq_masters = commit_queue.get_master_builder_map(
4556 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004557 for master, builders in cq_masters.iteritems():
4558 for builder in builders:
4559 # Skip presubmit builders, because these will fail without LGTM.
machenbach@chromium.org2403e802016-04-29 12:34:42 +00004560 masters.setdefault(master, {})[builder] = ['defaulttests']
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004561 if masters:
tandriib93dd2b2016-06-07 08:03:08 -07004562 print('Loaded default bots from CQ config (%s)' % cq_cfg)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004563 return masters
tandriib93dd2b2016-06-07 08:03:08 -07004564 else:
4565 print('CQ config exists (%s) but has no try bots listed' % cq_cfg)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004566
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004567 if not options.bot:
4568 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00004569
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004570 builders_and_tests = {}
4571 # TODO(machenbach): The old style command-line options don't support
4572 # multiple try masters yet.
4573 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4574 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4575
4576 for bot in old_style:
4577 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004578 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004579 elif ',' in bot:
4580 parser.error('Specify one bot per --bot flag')
4581 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004582 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004583
4584 for bot, tests in new_style:
4585 builders_and_tests.setdefault(bot, []).extend(tests)
4586
4587 # Return a master map with one master to be backwards compatible. The
4588 # master name defaults to an empty string, which will cause the master
4589 # not to be set on rietveld (deprecated).
4590 return {options.master: builders_and_tests}
4591
4592 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004593
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004594 for builders in masters.itervalues():
4595 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004596 print('ERROR You are trying to send a job to a triggered bot. This type '
4597 'of bot requires an\ninitial job from a parent (usually a builder).'
4598 ' Instead send your job to the parent.\n'
4599 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004600 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004601
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004602 patchset = cl.GetMostRecentPatchset()
4603 if patchset and patchset != cl.GetPatchset():
4604 print(
4605 '\nWARNING Mismatch between local config and server. Did a previous '
4606 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4607 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004608 if options.luci:
4609 trigger_luci_job(cl, masters, options)
4610 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004611 try:
4612 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4613 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004614 print('ERROR: %s' % ex)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004615 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004616 except Exception as e:
4617 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
vapiera7fbd5a2016-06-16 09:17:49 -07004618 print('ERROR: Exception when trying to trigger tryjobs: %s\n%s' %
4619 (e, stacktrace))
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004620 return 1
4621 else:
4622 try:
4623 cl.RpcServer().trigger_distributed_try_jobs(
4624 cl.GetIssue(), patchset, options.name, options.clobber,
4625 options.revision, masters)
4626 except urllib2.HTTPError as e:
4627 if e.code == 404:
4628 print('404 from rietveld; '
4629 'did you mean to use "git try" instead of "git cl try"?')
4630 return 1
4631 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004632
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004633 for (master, builders) in sorted(masters.iteritems()):
4634 if master:
vapiera7fbd5a2016-06-16 09:17:49 -07004635 print('Master: %s' % master)
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004636 length = max(len(builder) for builder in builders)
4637 for builder in sorted(builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004638 print(' %*s: %s' % (length, builder, ','.join(builders[builder])))
maruel@chromium.org15192402012-09-06 12:38:29 +00004639 return 0
4640
4641
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004642def CMDtry_results(parser, args):
4643 group = optparse.OptionGroup(parser, "Try job results options")
4644 group.add_option(
4645 "-p", "--patchset", type=int, help="patchset number if not current.")
4646 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004647 "--print-master", action='store_true', help="print master name as well.")
4648 group.add_option(
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004649 "--color", action='store_true', default=setup_color.IS_TTY,
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004650 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004651 group.add_option(
4652 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4653 help="Host of buildbucket. The default host is %default.")
4654 parser.add_option_group(group)
4655 auth.add_auth_options(parser)
4656 options, args = parser.parse_args(args)
4657 if args:
4658 parser.error('Unrecognized args: %s' % ' '.join(args))
4659
4660 auth_config = auth.extract_auth_config_from_options(options)
4661 cl = Changelist(auth_config=auth_config)
4662 if not cl.GetIssue():
4663 parser.error('Need to upload first')
4664
4665 if not options.patchset:
4666 options.patchset = cl.GetMostRecentPatchset()
4667 if options.patchset and options.patchset != cl.GetPatchset():
4668 print(
4669 '\nWARNING Mismatch between local config and server. Did a previous '
4670 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4671 'Continuing using\npatchset %s.\n' % options.patchset)
4672 try:
4673 jobs = fetch_try_jobs(auth_config, cl, options)
4674 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004675 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004676 return 1
4677 except Exception as e:
4678 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
vapiera7fbd5a2016-06-16 09:17:49 -07004679 print('ERROR: Exception when trying to fetch tryjobs: %s\n%s' %
4680 (e, stacktrace))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004681 return 1
4682 print_tryjobs(options, jobs)
4683 return 0
4684
4685
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004686@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004687def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004688 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004689 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004690 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004691 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004692
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004693 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004694 if args:
4695 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004696 branch = cl.GetBranch()
4697 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004698 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004699 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004700
4701 # Clear configured merge-base, if there is one.
4702 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004703 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004704 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004705 return 0
4706
4707
thestig@chromium.org00858c82013-12-02 23:08:03 +00004708def CMDweb(parser, args):
4709 """Opens the current CL in the web browser."""
4710 _, args = parser.parse_args(args)
4711 if args:
4712 parser.error('Unrecognized args: %s' % ' '.join(args))
4713
4714 issue_url = Changelist().GetIssueURL()
4715 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004716 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004717 return 1
4718
4719 webbrowser.open(issue_url)
4720 return 0
4721
4722
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004723def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004724 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004725 parser.add_option('-d', '--dry-run', action='store_true',
4726 help='trigger in dry run mode')
4727 parser.add_option('-c', '--clear', action='store_true',
4728 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004729 auth.add_auth_options(parser)
4730 options, args = parser.parse_args(args)
4731 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004732 if args:
4733 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004734 if options.dry_run and options.clear:
4735 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4736
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004737 cl = Changelist(auth_config=auth_config)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004738 if options.clear:
4739 state = _CQState.CLEAR
4740 elif options.dry_run:
4741 state = _CQState.DRY_RUN
4742 else:
4743 state = _CQState.COMMIT
4744 if not cl.GetIssue():
4745 parser.error('Must upload the issue first')
4746 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004747 return 0
4748
4749
groby@chromium.org411034a2013-02-26 15:12:01 +00004750def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004751 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004752 auth.add_auth_options(parser)
4753 options, args = parser.parse_args(args)
4754 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004755 if args:
4756 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004757 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00004758 # Ensure there actually is an issue to close.
4759 cl.GetDescription()
4760 cl.CloseIssue()
4761 return 0
4762
4763
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004764def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004765 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004766 auth.add_auth_options(parser)
4767 options, args = parser.parse_args(args)
4768 auth_config = auth.extract_auth_config_from_options(options)
4769 if args:
4770 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004771
4772 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004773 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004774 # Staged changes would be committed along with the patch from last
4775 # upload, hence counted toward the "last upload" side in the final
4776 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004777 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004778 return 1
4779
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004780 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004781 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004782 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004783 if not issue:
4784 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004785 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004786 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004787
4788 # Create a new branch based on the merge-base
4789 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004790 # Clear cached branch in cl object, to avoid overwriting original CL branch
4791 # properties.
4792 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004793 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004794 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004795 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004796 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004797 return rtn
4798
wychen@chromium.org06928532015-02-03 02:11:29 +00004799 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004800 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00004801 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004802 finally:
4803 RunGit(['checkout', '-q', branch])
4804 RunGit(['branch', '-D', TMP_BRANCH])
4805
4806 return 0
4807
4808
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004809def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004810 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004811 parser.add_option(
4812 '--no-color',
4813 action='store_true',
4814 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004815 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004816 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004817 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004818
4819 author = RunGit(['config', 'user.email']).strip() or None
4820
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004821 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004822
4823 if args:
4824 if len(args) > 1:
4825 parser.error('Unknown args')
4826 base_branch = args[0]
4827 else:
4828 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004829 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004830
4831 change = cl.GetChange(base_branch, None)
4832 return owners_finder.OwnersFinder(
4833 [f.LocalPath() for f in
4834 cl.GetChange(base_branch, None).AffectedFiles()],
4835 change.RepositoryRoot(), author,
4836 fopen=file, os_path=os.path, glob=glob.glob,
4837 disable_color=options.no_color).run()
4838
4839
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004840def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004841 """Generates a diff command."""
4842 # Generate diff for the current branch's changes.
4843 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4844 upstream_commit, '--' ]
4845
4846 if args:
4847 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004848 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004849 diff_cmd.append(arg)
4850 else:
4851 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004852
4853 return diff_cmd
4854
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004855def MatchingFileType(file_name, extensions):
4856 """Returns true if the file name ends with one of the given extensions."""
4857 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004858
enne@chromium.org555cfe42014-01-29 18:21:39 +00004859@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004860def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004861 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00004862 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07004863 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004864 parser.add_option('--full', action='store_true',
4865 help='Reformat the full content of all touched files')
4866 parser.add_option('--dry-run', action='store_true',
4867 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004868 parser.add_option('--python', action='store_true',
4869 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004870 parser.add_option('--diff', action='store_true',
4871 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004872 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004873
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004874 # git diff generates paths against the root of the repository. Change
4875 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004876 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004877 if rel_base_path:
4878 os.chdir(rel_base_path)
4879
digit@chromium.org29e47272013-05-17 17:01:46 +00004880 # Grab the merge-base commit, i.e. the upstream commit of the current
4881 # branch when it was created or the last time it was rebased. This is
4882 # to cover the case where the user may have called "git fetch origin",
4883 # moving the origin branch to a newer commit, but hasn't rebased yet.
4884 upstream_commit = None
4885 cl = Changelist()
4886 upstream_branch = cl.GetUpstreamBranch()
4887 if upstream_branch:
4888 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4889 upstream_commit = upstream_commit.strip()
4890
4891 if not upstream_commit:
4892 DieWithError('Could not find base commit for this branch. '
4893 'Are you in detached state?')
4894
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004895 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4896 diff_output = RunGit(changed_files_cmd)
4897 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004898 # Filter out files deleted by this CL
4899 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004900
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004901 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4902 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4903 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004904 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004905
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004906 top_dir = os.path.normpath(
4907 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
4908
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004909 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4910 # formatted. This is used to block during the presubmit.
4911 return_value = 0
4912
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004913 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004914 # Locate the clang-format binary in the checkout
4915 try:
4916 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07004917 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004918 DieWithError(e)
4919
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004920 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004921 cmd = [clang_format_tool]
4922 if not opts.dry_run and not opts.diff:
4923 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004924 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004925 if opts.diff:
4926 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004927 else:
4928 env = os.environ.copy()
4929 env['PATH'] = str(os.path.dirname(clang_format_tool))
4930 try:
4931 script = clang_format.FindClangFormatScriptInChromiumTree(
4932 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07004933 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004934 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004935
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004936 cmd = [sys.executable, script, '-p0']
4937 if not opts.dry_run and not opts.diff:
4938 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004939
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004940 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
4941 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004942
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004943 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
4944 if opts.diff:
4945 sys.stdout.write(stdout)
4946 if opts.dry_run and len(stdout) > 0:
4947 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004948
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004949 # Similar code to above, but using yapf on .py files rather than clang-format
4950 # on C/C++ files
4951 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004952 yapf_tool = gclient_utils.FindExecutable('yapf')
4953 if yapf_tool is None:
4954 DieWithError('yapf not found in PATH')
4955
4956 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004957 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004958 cmd = [yapf_tool]
4959 if not opts.dry_run and not opts.diff:
4960 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004961 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004962 if opts.diff:
4963 sys.stdout.write(stdout)
4964 else:
4965 # TODO(sbc): yapf --lines mode still has some issues.
4966 # https://github.com/google/yapf/issues/154
4967 DieWithError('--python currently only works with --full')
4968
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004969 # Dart's formatter does not have the nice property of only operating on
4970 # modified chunks, so hard code full.
4971 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004972 try:
4973 command = [dart_format.FindDartFmtToolInChromiumTree()]
4974 if not opts.dry_run and not opts.diff:
4975 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004976 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004977
ppi@chromium.org6593d932016-03-03 15:41:15 +00004978 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004979 if opts.dry_run and stdout:
4980 return_value = 2
4981 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07004982 print('Warning: Unable to check Dart code formatting. Dart SDK not '
4983 'found in this checkout. Files in other languages are still '
4984 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004985
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004986 # Format GN build files. Always run on full build files for canonical form.
4987 if gn_diff_files:
4988 cmd = ['gn', 'format']
4989 if not opts.dry_run and not opts.diff:
4990 cmd.append('--in-place')
4991 for gn_diff_file in gn_diff_files:
bsep@chromium.org627d9002016-04-29 00:00:52 +00004992 stdout = RunCommand(cmd + [gn_diff_file],
4993 shell=sys.platform == 'win32',
4994 cwd=top_dir)
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004995 if opts.diff:
4996 sys.stdout.write(stdout)
4997
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004998 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004999
5000
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005001@subcommand.usage('<codereview url or issue id>')
5002def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005003 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005004 _, args = parser.parse_args(args)
5005
5006 if len(args) != 1:
5007 parser.print_help()
5008 return 1
5009
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005010 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005011 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005012 parser.print_help()
5013 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005014 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005015
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005016 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005017 output = RunGit(['config', '--local', '--get-regexp',
5018 r'branch\..*\.%s' % issueprefix],
5019 error_ok=True)
5020 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005021 if issue == target_issue:
5022 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005023
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005024 branches = []
5025 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00005026 branches.extend(find_issues(cls.IssueSettingSuffix()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005027 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005028 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005029 return 1
5030 if len(branches) == 1:
5031 RunGit(['checkout', branches[0]])
5032 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005033 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005034 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005035 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005036 which = raw_input('Choose by index: ')
5037 try:
5038 RunGit(['checkout', branches[int(which)]])
5039 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005040 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005041 return 1
5042
5043 return 0
5044
5045
maruel@chromium.org29404b52014-09-08 22:58:00 +00005046def CMDlol(parser, args):
5047 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005048 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005049 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5050 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5051 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005052 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005053 return 0
5054
5055
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005056class OptionParser(optparse.OptionParser):
5057 """Creates the option parse and add --verbose support."""
5058 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005059 optparse.OptionParser.__init__(
5060 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005061 self.add_option(
5062 '-v', '--verbose', action='count', default=0,
5063 help='Use 2 times for more debugging info')
5064
5065 def parse_args(self, args=None, values=None):
5066 options, args = optparse.OptionParser.parse_args(self, args, values)
5067 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5068 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5069 return options, args
5070
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005071
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005072def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005073 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005074 print('\nYour python version %s is unsupported, please upgrade.\n' %
5075 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005076 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005077
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005078 # Reload settings.
5079 global settings
5080 settings = Settings()
5081
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005082 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005083 dispatcher = subcommand.CommandDispatcher(__name__)
5084 try:
5085 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005086 except auth.AuthenticationError as e:
5087 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005088 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005089 if e.code != 500:
5090 raise
5091 DieWithError(
5092 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5093 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005094 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005095
5096
5097if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005098 # These affect sys.stdout so do it outside of main() to simplify mocks in
5099 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005100 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005101 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005102 try:
5103 sys.exit(main(sys.argv[1:]))
5104 except KeyboardInterrupt:
5105 sys.stderr.write('interrupted\n')
5106 sys.exit(1)