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