blob: f32709728b53e7fdcb6b818d20dc549b8f74ad33 [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():
nodirca166002016-06-27 10:59:51 -07001901 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001902 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:
nodirca166002016-06-27 10:59:51 -07001909 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001910 upload_args.extend(['--title', options.title])
1911 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 self.GetIssue():
2405 # Try to get the message from a previous upload.
2406 message = self.GetDescription()
2407 if not message:
2408 DieWithError(
2409 'failed to fetch description from current Gerrit issue %d\n'
2410 '%s' % (self.GetIssue(), self.GetIssueURL()))
2411 change_id = self._GetChangeDetail()['change_id']
2412 while True:
2413 footer_change_ids = git_footers.get_footer_change_id(message)
2414 if footer_change_ids == [change_id]:
2415 break
2416 if not footer_change_ids:
2417 message = git_footers.add_footer_change_id(message, change_id)
2418 print('WARNING: appended missing Change-Id to issue description')
2419 continue
2420 # There is already a valid footer but with different or several ids.
2421 # Doing this automatically is non-trivial as we don't want to lose
2422 # existing other footers, yet we want to append just 1 desired
2423 # Change-Id. Thus, just create a new footer, but let user verify the
2424 # new description.
2425 message = '%s\n\nChange-Id: %s' % (message, change_id)
2426 print(
2427 'WARNING: issue %s has Change-Id footer(s):\n'
2428 ' %s\n'
2429 'but issue has Change-Id %s, according to Gerrit.\n'
2430 'Please, check the proposed correction to the description, '
2431 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2432 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2433 change_id))
2434 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2435 if not options.force:
2436 change_desc = ChangeDescription(message)
2437 change_desc.prompt()
2438 message = change_desc.description
2439 if not message:
2440 DieWithError("Description is empty. Aborting...")
2441 # Continue the while loop.
2442 # Sanity check of this code - we should end up with proper message
2443 # footer.
2444 assert [change_id] == git_footers.get_footer_change_id(message)
2445 change_desc = ChangeDescription(message)
2446 else:
2447 change_desc = ChangeDescription(
2448 options.message or CreateDescriptionFromLog(args))
2449 if not options.force:
2450 change_desc.prompt()
2451 if not change_desc.description:
2452 DieWithError("Description is empty. Aborting...")
2453 message = change_desc.description
2454 change_ids = git_footers.get_footer_change_id(message)
2455 if len(change_ids) > 1:
2456 DieWithError('too many Change-Id footers, at most 1 allowed.')
2457 if not change_ids:
2458 # Generate the Change-Id automatically.
2459 message = git_footers.add_footer_change_id(
2460 message, GenerateGerritChangeId(message))
2461 change_desc.set_description(message)
2462 change_ids = git_footers.get_footer_change_id(message)
2463 assert len(change_ids) == 1
2464 change_id = change_ids[0]
2465
2466 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2467 if remote is '.':
2468 # If our upstream branch is local, we base our squashed commit on its
2469 # squashed version.
2470 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2471 # Check the squashed hash of the parent.
2472 parent = RunGit(['config',
2473 'branch.%s.gerritsquashhash' % upstream_branch_name],
2474 error_ok=True).strip()
2475 # Verify that the upstream branch has been uploaded too, otherwise
2476 # Gerrit will create additional CLs when uploading.
2477 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2478 RunGitSilent(['rev-parse', parent + ':'])):
2479 # TODO(tandrii): remove "old depot_tools" part on April 12, 2016.
2480 DieWithError(
2481 'Upload upstream branch %s first.\n'
2482 'Note: maybe you\'ve uploaded it with --no-squash or with an old '
2483 'version of depot_tools. If so, then re-upload it with:\n'
2484 ' git cl upload --squash\n' % upstream_branch_name)
2485 else:
2486 parent = self.GetCommonAncestorWithUpstream()
2487
2488 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2489 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2490 '-m', message]).strip()
2491 else:
2492 change_desc = ChangeDescription(
2493 options.message or CreateDescriptionFromLog(args))
2494 if not change_desc.description:
2495 DieWithError("Description is empty. Aborting...")
2496
2497 if not git_footers.get_footer_change_id(change_desc.description):
2498 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002499 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2500 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002501 ref_to_push = 'HEAD'
2502 parent = '%s/%s' % (gerrit_remote, branch)
2503 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2504
2505 assert change_desc
2506 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2507 ref_to_push)]).splitlines()
2508 if len(commits) > 1:
2509 print('WARNING: This will upload %d commits. Run the following command '
2510 'to see which commits will be uploaded: ' % len(commits))
2511 print('git log %s..%s' % (parent, ref_to_push))
2512 print('You can also use `git squash-branch` to squash these into a '
2513 'single commit.')
2514 ask_for_data('About to upload; enter to confirm.')
2515
2516 if options.reviewers or options.tbr_owners:
2517 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2518 change)
2519
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002520 # Extra options that can be specified at push time. Doc:
2521 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2522 refspec_opts = []
2523 if options.title:
2524 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2525 # reverse on its side.
2526 if '_' in options.title:
2527 print('WARNING: underscores in title will be converted to spaces.')
2528 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2529
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002530 if options.send_mail:
2531 if not change_desc.get_reviewers():
2532 DieWithError('Must specify reviewers to send email.')
2533 refspec_opts.append('notify=ALL')
2534 else:
2535 refspec_opts.append('notify=NONE')
2536
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002537 cc = self.GetCCList().split(',')
2538 if options.cc:
2539 cc.extend(options.cc)
2540 cc = filter(None, cc)
2541 if cc:
tandrii@chromium.org074c2af2016-06-03 23:18:40 +00002542 refspec_opts.extend('cc=' + email.strip() for email in cc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002543
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002544 if change_desc.get_reviewers():
2545 refspec_opts.extend('r=' + email.strip()
2546 for email in change_desc.get_reviewers())
2547
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002548 refspec_suffix = ''
2549 if refspec_opts:
2550 refspec_suffix = '%' + ','.join(refspec_opts)
2551 assert ' ' not in refspec_suffix, (
2552 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002553 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002554
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002555 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002556 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002557 print_stdout=True,
2558 # Flush after every line: useful for seeing progress when running as
2559 # recipe.
2560 filter_fn=lambda _: sys.stdout.flush())
2561
2562 if options.squash:
2563 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2564 change_numbers = [m.group(1)
2565 for m in map(regex.match, push_stdout.splitlines())
2566 if m]
2567 if len(change_numbers) != 1:
2568 DieWithError(
2569 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2570 'Change-Id: %s') % (len(change_numbers), change_id))
2571 self.SetIssue(change_numbers[0])
2572 RunGit(['config', 'branch.%s.gerritsquashhash' % self.GetBranch(),
2573 ref_to_push])
2574 return 0
2575
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002576 def _AddChangeIdToCommitMessage(self, options, args):
2577 """Re-commits using the current message, assumes the commit hook is in
2578 place.
2579 """
2580 log_desc = options.message or CreateDescriptionFromLog(args)
2581 git_command = ['commit', '--amend', '-m', log_desc]
2582 RunGit(git_command)
2583 new_log_desc = CreateDescriptionFromLog(args)
2584 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002585 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002586 return new_log_desc
2587 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002588 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002589
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002590 def SetCQState(self, new_state):
2591 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
2592 # TODO(tandrii): maybe allow configurability in codereview.settings or by
2593 # self-discovery of label config for this CL using REST API.
2594 vote_map = {
2595 _CQState.NONE: 0,
2596 _CQState.DRY_RUN: 1,
2597 _CQState.COMMIT : 2,
2598 }
2599 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2600 labels={'Commit-Queue': vote_map[new_state]})
2601
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002602
2603_CODEREVIEW_IMPLEMENTATIONS = {
2604 'rietveld': _RietveldChangelistImpl,
2605 'gerrit': _GerritChangelistImpl,
2606}
2607
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002608
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002609def _add_codereview_select_options(parser):
2610 """Appends --gerrit and --rietveld options to force specific codereview."""
2611 parser.codereview_group = optparse.OptionGroup(
2612 parser, 'EXPERIMENTAL! Codereview override options')
2613 parser.add_option_group(parser.codereview_group)
2614 parser.codereview_group.add_option(
2615 '--gerrit', action='store_true',
2616 help='Force the use of Gerrit for codereview')
2617 parser.codereview_group.add_option(
2618 '--rietveld', action='store_true',
2619 help='Force the use of Rietveld for codereview')
2620
2621
2622def _process_codereview_select_options(parser, options):
2623 if options.gerrit and options.rietveld:
2624 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2625 options.forced_codereview = None
2626 if options.gerrit:
2627 options.forced_codereview = 'gerrit'
2628 elif options.rietveld:
2629 options.forced_codereview = 'rietveld'
2630
2631
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002632class ChangeDescription(object):
2633 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002634 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002635 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002636
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002637 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002638 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002639
agable@chromium.org42c20792013-09-12 17:34:49 +00002640 @property # www.logilab.org/ticket/89786
2641 def description(self): # pylint: disable=E0202
2642 return '\n'.join(self._description_lines)
2643
2644 def set_description(self, desc):
2645 if isinstance(desc, basestring):
2646 lines = desc.splitlines()
2647 else:
2648 lines = [line.rstrip() for line in desc]
2649 while lines and not lines[0]:
2650 lines.pop(0)
2651 while lines and not lines[-1]:
2652 lines.pop(-1)
2653 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002654
piman@chromium.org336f9122014-09-04 02:16:55 +00002655 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002656 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002657 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002658 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002659 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002660 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002661
agable@chromium.org42c20792013-09-12 17:34:49 +00002662 # Get the set of R= and TBR= lines and remove them from the desciption.
2663 regexp = re.compile(self.R_LINE)
2664 matches = [regexp.match(line) for line in self._description_lines]
2665 new_desc = [l for i, l in enumerate(self._description_lines)
2666 if not matches[i]]
2667 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002668
agable@chromium.org42c20792013-09-12 17:34:49 +00002669 # Construct new unified R= and TBR= lines.
2670 r_names = []
2671 tbr_names = []
2672 for match in matches:
2673 if not match:
2674 continue
2675 people = cleanup_list([match.group(2).strip()])
2676 if match.group(1) == 'TBR':
2677 tbr_names.extend(people)
2678 else:
2679 r_names.extend(people)
2680 for name in r_names:
2681 if name not in reviewers:
2682 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002683 if add_owners_tbr:
2684 owners_db = owners.Database(change.RepositoryRoot(),
2685 fopen=file, os_path=os.path, glob=glob.glob)
2686 all_reviewers = set(tbr_names + reviewers)
2687 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2688 all_reviewers)
2689 tbr_names.extend(owners_db.reviewers_for(missing_files,
2690 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002691 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2692 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2693
2694 # Put the new lines in the description where the old first R= line was.
2695 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2696 if 0 <= line_loc < len(self._description_lines):
2697 if new_tbr_line:
2698 self._description_lines.insert(line_loc, new_tbr_line)
2699 if new_r_line:
2700 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002701 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002702 if new_r_line:
2703 self.append_footer(new_r_line)
2704 if new_tbr_line:
2705 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002706
2707 def prompt(self):
2708 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002709 self.set_description([
2710 '# Enter a description of the change.',
2711 '# This will be displayed on the codereview site.',
2712 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002713 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002714 '--------------------',
2715 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002716
agable@chromium.org42c20792013-09-12 17:34:49 +00002717 regexp = re.compile(self.BUG_LINE)
2718 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00002719 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00002720 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002721 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002722 if not content:
2723 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002724 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002725
2726 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002727 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2728 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002729 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002730 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002731
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002732 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00002733 """Adds a footer line to the description.
2734
2735 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
2736 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
2737 that Gerrit footers are always at the end.
2738 """
2739 parsed_footer_line = git_footers.parse_footer(line)
2740 if parsed_footer_line:
2741 # Line is a gerrit footer in the form: Footer-Key: any value.
2742 # Thus, must be appended observing Gerrit footer rules.
2743 self.set_description(
2744 git_footers.add_footer(self.description,
2745 key=parsed_footer_line[0],
2746 value=parsed_footer_line[1]))
2747 return
2748
2749 if not self._description_lines:
2750 self._description_lines.append(line)
2751 return
2752
2753 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
2754 if gerrit_footers:
2755 # git_footers.split_footers ensures that there is an empty line before
2756 # actual (gerrit) footers, if any. We have to keep it that way.
2757 assert top_lines and top_lines[-1] == ''
2758 top_lines, separator = top_lines[:-1], top_lines[-1:]
2759 else:
2760 separator = [] # No need for separator if there are no gerrit_footers.
2761
2762 prev_line = top_lines[-1] if top_lines else ''
2763 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
2764 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2765 top_lines.append('')
2766 top_lines.append(line)
2767 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002768
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002769 def get_reviewers(self):
2770 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002771 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
2772 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002773 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002774
2775
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002776def get_approving_reviewers(props):
2777 """Retrieves the reviewers that approved a CL from the issue properties with
2778 messages.
2779
2780 Note that the list may contain reviewers that are not committer, thus are not
2781 considered by the CQ.
2782 """
2783 return sorted(
2784 set(
2785 message['sender']
2786 for message in props['messages']
2787 if message['approval'] and message['sender'] in props['reviewers']
2788 )
2789 )
2790
2791
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002792def FindCodereviewSettingsFile(filename='codereview.settings'):
2793 """Finds the given file starting in the cwd and going up.
2794
2795 Only looks up to the top of the repository unless an
2796 'inherit-review-settings-ok' file exists in the root of the repository.
2797 """
2798 inherit_ok_file = 'inherit-review-settings-ok'
2799 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002800 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002801 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2802 root = '/'
2803 while True:
2804 if filename in os.listdir(cwd):
2805 if os.path.isfile(os.path.join(cwd, filename)):
2806 return open(os.path.join(cwd, filename))
2807 if cwd == root:
2808 break
2809 cwd = os.path.dirname(cwd)
2810
2811
2812def LoadCodereviewSettingsFromFile(fileobj):
2813 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002814 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002815
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002816 def SetProperty(name, setting, unset_error_ok=False):
2817 fullname = 'rietveld.' + name
2818 if setting in keyvals:
2819 RunGit(['config', fullname, keyvals[setting]])
2820 else:
2821 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2822
2823 SetProperty('server', 'CODE_REVIEW_SERVER')
2824 # Only server setting is required. Other settings can be absent.
2825 # In that case, we ignore errors raised during option deletion attempt.
2826 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002827 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002828 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2829 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002830 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002831 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002832 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2833 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002834 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002835 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002836 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002837 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2838 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002839
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002840 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002841 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002842
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002843 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07002844 RunGit(['config', 'gerrit.squash-uploads',
2845 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002846
tandrii@chromium.org28253532016-04-14 13:46:56 +00002847 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00002848 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00002849 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
2850
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002851 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2852 #should be of the form
2853 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2854 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2855 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2856 keyvals['ORIGIN_URL_CONFIG']])
2857
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002858
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002859def urlretrieve(source, destination):
2860 """urllib is broken for SSL connections via a proxy therefore we
2861 can't use urllib.urlretrieve()."""
2862 with open(destination, 'w') as f:
2863 f.write(urllib2.urlopen(source).read())
2864
2865
ukai@chromium.org712d6102013-11-27 00:52:58 +00002866def hasSheBang(fname):
2867 """Checks fname is a #! script."""
2868 with open(fname) as f:
2869 return f.read(2).startswith('#!')
2870
2871
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00002872# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
2873def DownloadHooks(*args, **kwargs):
2874 pass
2875
2876
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002877def DownloadGerritHook(force):
2878 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002879
2880 Args:
2881 force: True to update hooks. False to install hooks if not present.
2882 """
2883 if not settings.GetIsGerrit():
2884 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00002885 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002886 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2887 if not os.access(dst, os.X_OK):
2888 if os.path.exists(dst):
2889 if not force:
2890 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002891 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002892 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002893 if not hasSheBang(dst):
2894 DieWithError('Not a script: %s\n'
2895 'You need to download from\n%s\n'
2896 'into .git/hooks/commit-msg and '
2897 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002898 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2899 except Exception:
2900 if os.path.exists(dst):
2901 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002902 DieWithError('\nFailed to download hooks.\n'
2903 'You need to download from\n%s\n'
2904 'into .git/hooks/commit-msg and '
2905 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002906
2907
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002908
2909def GetRietveldCodereviewSettingsInteractively():
2910 """Prompt the user for settings."""
2911 server = settings.GetDefaultServerUrl(error_ok=True)
2912 prompt = 'Rietveld server (host[:port])'
2913 prompt += ' [%s]' % (server or DEFAULT_SERVER)
2914 newserver = ask_for_data(prompt + ':')
2915 if not server and not newserver:
2916 newserver = DEFAULT_SERVER
2917 if newserver:
2918 newserver = gclient_utils.UpgradeToHttps(newserver)
2919 if newserver != server:
2920 RunGit(['config', 'rietveld.server', newserver])
2921
2922 def SetProperty(initial, caption, name, is_url):
2923 prompt = caption
2924 if initial:
2925 prompt += ' ("x" to clear) [%s]' % initial
2926 new_val = ask_for_data(prompt + ':')
2927 if new_val == 'x':
2928 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
2929 elif new_val:
2930 if is_url:
2931 new_val = gclient_utils.UpgradeToHttps(new_val)
2932 if new_val != initial:
2933 RunGit(['config', 'rietveld.' + name, new_val])
2934
2935 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
2936 SetProperty(settings.GetDefaultPrivateFlag(),
2937 'Private flag (rietveld only)', 'private', False)
2938 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
2939 'tree-status-url', False)
2940 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
2941 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
2942 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
2943 'run-post-upload-hook', False)
2944
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002945@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002946def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002947 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002948
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002949 print('WARNING: git cl config works for Rietveld only.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002950 'For Gerrit, see http://crbug.com/603116.')
2951 # TODO(tandrii): add Gerrit support as part of http://crbug.com/603116.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00002952 parser.add_option('--activate-update', action='store_true',
2953 help='activate auto-updating [rietveld] section in '
2954 '.git/config')
2955 parser.add_option('--deactivate-update', action='store_true',
2956 help='deactivate auto-updating [rietveld] section in '
2957 '.git/config')
2958 options, args = parser.parse_args(args)
2959
2960 if options.deactivate_update:
2961 RunGit(['config', 'rietveld.autoupdate', 'false'])
2962 return
2963
2964 if options.activate_update:
2965 RunGit(['config', '--unset', 'rietveld.autoupdate'])
2966 return
2967
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002968 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002969 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002970 return 0
2971
2972 url = args[0]
2973 if not url.endswith('codereview.settings'):
2974 url = os.path.join(url, 'codereview.settings')
2975
2976 # Load code review settings and download hooks (if available).
2977 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
2978 return 0
2979
2980
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002981def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002982 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002983 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
2984 branch = ShortBranchName(branchref)
2985 _, args = parser.parse_args(args)
2986 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07002987 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002988 return RunGit(['config', 'branch.%s.base-url' % branch],
2989 error_ok=False).strip()
2990 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002991 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002992 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
2993 error_ok=False).strip()
2994
2995
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002996def color_for_status(status):
2997 """Maps a Changelist status to color, for CMDstatus and other tools."""
2998 return {
2999 'unsent': Fore.RED,
3000 'waiting': Fore.BLUE,
3001 'reply': Fore.YELLOW,
3002 'lgtm': Fore.GREEN,
3003 'commit': Fore.MAGENTA,
3004 'closed': Fore.CYAN,
3005 'error': Fore.WHITE,
3006 }.get(status, Fore.WHITE)
3007
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003008
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003009def get_cl_statuses(changes, fine_grained, max_processes=None):
3010 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003011
3012 If fine_grained is true, this will fetch CL statuses from the server.
3013 Otherwise, simply indicate if there's a matching url for the given branches.
3014
3015 If max_processes is specified, it is used as the maximum number of processes
3016 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3017 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003018
3019 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003020 """
3021 # Silence upload.py otherwise it becomes unwieldly.
3022 upload.verbosity = 0
3023
3024 if fine_grained:
3025 # Process one branch synchronously to work through authentication, then
3026 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003027 if changes:
3028 fetch = lambda cl: (cl, cl.GetStatus())
3029 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003030
kmarshall3bff56b2016-06-06 18:31:47 -07003031 if not changes:
3032 # Exit early if there was only one branch to fetch.
3033 return
3034
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003035 changes_to_fetch = changes[1:]
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003036 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003037 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003038 if max_processes is not None
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003039 else len(changes_to_fetch))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003040
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003041 fetched_cls = set()
3042 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003043 while True:
3044 try:
3045 row = it.next(timeout=5)
3046 except multiprocessing.TimeoutError:
3047 break
3048
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003049 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003050 yield row
3051
3052 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003053 for cl in set(changes_to_fetch) - fetched_cls:
3054 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003055
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003056 else:
3057 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003058 for cl in changes:
3059 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003060
rmistry@google.com2dd99862015-06-22 12:22:18 +00003061
3062def upload_branch_deps(cl, args):
3063 """Uploads CLs of local branches that are dependents of the current branch.
3064
3065 If the local branch dependency tree looks like:
3066 test1 -> test2.1 -> test3.1
3067 -> test3.2
3068 -> test2.2 -> test3.3
3069
3070 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3071 run on the dependent branches in this order:
3072 test2.1, test3.1, test3.2, test2.2, test3.3
3073
3074 Note: This function does not rebase your local dependent branches. Use it when
3075 you make a change to the parent branch that will not conflict with its
3076 dependent branches, and you would like their dependencies updated in
3077 Rietveld.
3078 """
3079 if git_common.is_dirty_git_tree('upload-branch-deps'):
3080 return 1
3081
3082 root_branch = cl.GetBranch()
3083 if root_branch is None:
3084 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3085 'Get on a branch!')
3086 if not cl.GetIssue() or not cl.GetPatchset():
3087 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3088 'patchset dependencies without an uploaded CL.')
3089
3090 branches = RunGit(['for-each-ref',
3091 '--format=%(refname:short) %(upstream:short)',
3092 'refs/heads'])
3093 if not branches:
3094 print('No local branches found.')
3095 return 0
3096
3097 # Create a dictionary of all local branches to the branches that are dependent
3098 # on it.
3099 tracked_to_dependents = collections.defaultdict(list)
3100 for b in branches.splitlines():
3101 tokens = b.split()
3102 if len(tokens) == 2:
3103 branch_name, tracked = tokens
3104 tracked_to_dependents[tracked].append(branch_name)
3105
vapiera7fbd5a2016-06-16 09:17:49 -07003106 print()
3107 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003108 dependents = []
3109 def traverse_dependents_preorder(branch, padding=''):
3110 dependents_to_process = tracked_to_dependents.get(branch, [])
3111 padding += ' '
3112 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003113 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003114 dependents.append(dependent)
3115 traverse_dependents_preorder(dependent, padding)
3116 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003117 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003118
3119 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003120 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003121 return 0
3122
vapiera7fbd5a2016-06-16 09:17:49 -07003123 print('This command will checkout all dependent branches and run '
3124 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003125 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3126
andybons@chromium.org962f9462016-02-03 20:00:42 +00003127 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003128 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003129 args.extend(['-t', 'Updated patchset dependency'])
3130
rmistry@google.com2dd99862015-06-22 12:22:18 +00003131 # Record all dependents that failed to upload.
3132 failures = {}
3133 # Go through all dependents, checkout the branch and upload.
3134 try:
3135 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003136 print()
3137 print('--------------------------------------')
3138 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003139 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003140 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003141 try:
3142 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003143 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003144 failures[dependent_branch] = 1
3145 except: # pylint: disable=W0702
3146 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003147 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003148 finally:
3149 # Swap back to the original root branch.
3150 RunGit(['checkout', '-q', root_branch])
3151
vapiera7fbd5a2016-06-16 09:17:49 -07003152 print()
3153 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003154 for dependent_branch in dependents:
3155 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003156 print(' %s : %s' % (dependent_branch, upload_status))
3157 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003158
3159 return 0
3160
3161
kmarshall3bff56b2016-06-06 18:31:47 -07003162def CMDarchive(parser, args):
3163 """Archives and deletes branches associated with closed changelists."""
3164 parser.add_option(
3165 '-j', '--maxjobs', action='store', type=int,
3166 help='The maximum number of jobs to use when retrieving review status')
3167 parser.add_option(
3168 '-f', '--force', action='store_true',
3169 help='Bypasses the confirmation prompt.')
3170
3171 auth.add_auth_options(parser)
3172 options, args = parser.parse_args(args)
3173 if args:
3174 parser.error('Unsupported args: %s' % ' '.join(args))
3175 auth_config = auth.extract_auth_config_from_options(options)
3176
3177 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3178 if not branches:
3179 return 0
3180
vapiera7fbd5a2016-06-16 09:17:49 -07003181 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003182 changes = [Changelist(branchref=b, auth_config=auth_config)
3183 for b in branches.splitlines()]
3184 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3185 statuses = get_cl_statuses(changes,
3186 fine_grained=True,
3187 max_processes=options.maxjobs)
3188 proposal = [(cl.GetBranch(),
3189 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3190 for cl, status in statuses
3191 if status == 'closed']
3192 proposal.sort()
3193
3194 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003195 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003196 return 0
3197
3198 current_branch = GetCurrentBranch()
3199
vapiera7fbd5a2016-06-16 09:17:49 -07003200 print('\nBranches with closed issues that will be archived:\n')
3201 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
kmarshall3bff56b2016-06-06 18:31:47 -07003202 for next_item in proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003203 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003204
3205 if any(branch == current_branch for branch, _ in proposal):
3206 print('You are currently on a branch \'%s\' which is associated with a '
3207 'closed codereview issue, so archive cannot proceed. Please '
3208 'checkout another branch and run this command again.' %
3209 current_branch)
3210 return 1
3211
3212 if not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003213 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3214 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003215 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003216 return 1
3217
3218 for branch, tagname in proposal:
3219 RunGit(['tag', tagname, branch])
3220 RunGit(['branch', '-D', branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003221 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003222
3223 return 0
3224
3225
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003226def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003227 """Show status of changelists.
3228
3229 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003230 - Red not sent for review or broken
3231 - Blue waiting for review
3232 - Yellow waiting for you to reply to review
3233 - Green LGTM'ed
3234 - Magenta in the commit queue
3235 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003236
3237 Also see 'git cl comments'.
3238 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003239 parser.add_option('--field',
3240 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003241 parser.add_option('-f', '--fast', action='store_true',
3242 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003243 parser.add_option(
3244 '-j', '--maxjobs', action='store', type=int,
3245 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003246
3247 auth.add_auth_options(parser)
3248 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003249 if args:
3250 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003251 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003252
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003253 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003254 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003255 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003256 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003257 elif options.field == 'id':
3258 issueid = cl.GetIssue()
3259 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003260 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003261 elif options.field == 'patch':
3262 patchset = cl.GetPatchset()
3263 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003264 print(patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003265 elif options.field == 'url':
3266 url = cl.GetIssueURL()
3267 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003268 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003269 return 0
3270
3271 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3272 if not branches:
3273 print('No local branch found.')
3274 return 0
3275
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003276 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003277 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003278 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003279 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003280 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003281 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003282 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003283
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003284 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003285 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3286 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3287 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003288 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003289 c, status = output.next()
3290 branch_statuses[c.GetBranch()] = status
3291 status = branch_statuses.pop(branch)
3292 url = cl.GetIssueURL()
3293 if url and (not status or status == 'error'):
3294 # The issue probably doesn't exist anymore.
3295 url += ' (broken)'
3296
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003297 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003298 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003299 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003300 color = ''
3301 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003302 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003303 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003304 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003305 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003306
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003307 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003308 print()
3309 print('Current branch:',)
3310 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003311 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003312 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003313 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003314 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003315 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003316 print('Issue description:')
3317 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003318 return 0
3319
3320
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003321def colorize_CMDstatus_doc():
3322 """To be called once in main() to add colors to git cl status help."""
3323 colors = [i for i in dir(Fore) if i[0].isupper()]
3324
3325 def colorize_line(line):
3326 for color in colors:
3327 if color in line.upper():
3328 # Extract whitespaces first and the leading '-'.
3329 indent = len(line) - len(line.lstrip(' ')) + 1
3330 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3331 return line
3332
3333 lines = CMDstatus.__doc__.splitlines()
3334 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3335
3336
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003337@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003338def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003339 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003340
3341 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003342 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003343 parser.add_option('-r', '--reverse', action='store_true',
3344 help='Lookup the branch(es) for the specified issues. If '
3345 'no issues are specified, all branches with mapped '
3346 'issues will be listed.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003347 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003348 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003349 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003350
dnj@chromium.org406c4402015-03-03 17:22:28 +00003351 if options.reverse:
3352 branches = RunGit(['for-each-ref', 'refs/heads',
3353 '--format=%(refname:short)']).splitlines()
3354
3355 # Reverse issue lookup.
3356 issue_branch_map = {}
3357 for branch in branches:
3358 cl = Changelist(branchref=branch)
3359 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3360 if not args:
3361 args = sorted(issue_branch_map.iterkeys())
3362 for issue in args:
3363 if not issue:
3364 continue
vapiera7fbd5a2016-06-16 09:17:49 -07003365 print('Branch for issue number %s: %s' % (
3366 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
dnj@chromium.org406c4402015-03-03 17:22:28 +00003367 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003368 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003369 if len(args) > 0:
3370 try:
3371 issue = int(args[0])
3372 except ValueError:
3373 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003374 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003375 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003376 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003377 return 0
3378
3379
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003380def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003381 """Shows or posts review comments for any changelist."""
3382 parser.add_option('-a', '--add-comment', dest='comment',
3383 help='comment to add to an issue')
3384 parser.add_option('-i', dest='issue',
3385 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003386 parser.add_option('-j', '--json-file',
3387 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003388 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003389 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003390 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003391
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003392 issue = None
3393 if options.issue:
3394 try:
3395 issue = int(options.issue)
3396 except ValueError:
3397 DieWithError('A review issue id is expected to be a number')
3398
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003399 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003400
3401 if options.comment:
3402 cl.AddComment(options.comment)
3403 return 0
3404
3405 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003406 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003407 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003408 summary.append({
3409 'date': message['date'],
3410 'lgtm': False,
3411 'message': message['text'],
3412 'not_lgtm': False,
3413 'sender': message['sender'],
3414 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003415 if message['disapproval']:
3416 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003417 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003418 elif message['approval']:
3419 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003420 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003421 elif message['sender'] == data['owner_email']:
3422 color = Fore.MAGENTA
3423 else:
3424 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003425 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003426 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003427 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003428 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003429 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003430 if options.json_file:
3431 with open(options.json_file, 'wb') as f:
3432 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003433 return 0
3434
3435
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003436@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003437def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003438 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003439 parser.add_option('-d', '--display', action='store_true',
3440 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003441 parser.add_option('-n', '--new-description',
3442 help='New description to set for this issue (- for stdin)')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003443
3444 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003445 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003446 options, args = parser.parse_args(args)
3447 _process_codereview_select_options(parser, options)
3448
3449 target_issue = None
3450 if len(args) > 0:
3451 issue_arg = ParseIssueNumberArgument(args[0])
3452 if not issue_arg.valid:
3453 parser.print_help()
3454 return 1
3455 target_issue = issue_arg.issue
3456
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003457 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003458
3459 cl = Changelist(
3460 auth_config=auth_config, issue=target_issue,
3461 codereview=options.forced_codereview)
3462
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003463 if not cl.GetIssue():
3464 DieWithError('This branch has no associated changelist.')
3465 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003466
smut@google.com34fb6b12015-07-13 20:03:26 +00003467 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003468 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003469 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003470
3471 if options.new_description:
3472 text = options.new_description
3473 if text == '-':
3474 text = '\n'.join(l.rstrip() for l in sys.stdin)
3475
3476 description.set_description(text)
3477 else:
3478 description.prompt()
3479
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003480 if cl.GetDescription() != description.description:
3481 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003482 return 0
3483
3484
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003485def CreateDescriptionFromLog(args):
3486 """Pulls out the commit log to use as a base for the CL description."""
3487 log_args = []
3488 if len(args) == 1 and not args[0].endswith('.'):
3489 log_args = [args[0] + '..']
3490 elif len(args) == 1 and args[0].endswith('...'):
3491 log_args = [args[0][:-1]]
3492 elif len(args) == 2:
3493 log_args = [args[0] + '..' + args[1]]
3494 else:
3495 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003496 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003497
3498
thestig@chromium.org44202a22014-03-11 19:22:18 +00003499def CMDlint(parser, args):
3500 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003501 parser.add_option('--filter', action='append', metavar='-x,+y',
3502 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003503 auth.add_auth_options(parser)
3504 options, args = parser.parse_args(args)
3505 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003506
3507 # Access to a protected member _XX of a client class
3508 # pylint: disable=W0212
3509 try:
3510 import cpplint
3511 import cpplint_chromium
3512 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003513 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003514 return 1
3515
3516 # Change the current working directory before calling lint so that it
3517 # shows the correct base.
3518 previous_cwd = os.getcwd()
3519 os.chdir(settings.GetRoot())
3520 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003521 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003522 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3523 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003524 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003525 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003526 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003527
3528 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003529 command = args + files
3530 if options.filter:
3531 command = ['--filter=' + ','.join(options.filter)] + command
3532 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003533
3534 white_regex = re.compile(settings.GetLintRegex())
3535 black_regex = re.compile(settings.GetLintIgnoreRegex())
3536 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3537 for filename in filenames:
3538 if white_regex.match(filename):
3539 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07003540 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003541 else:
3542 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3543 extra_check_functions)
3544 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003545 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003546 finally:
3547 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07003548 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003549 if cpplint._cpplint_state.error_count != 0:
3550 return 1
3551 return 0
3552
3553
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003554def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003555 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003556 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003557 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003558 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003559 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003560 auth.add_auth_options(parser)
3561 options, args = parser.parse_args(args)
3562 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003563
sbc@chromium.org71437c02015-04-09 19:29:40 +00003564 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07003565 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003566 return 1
3567
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003568 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003569 if args:
3570 base_branch = args[0]
3571 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003572 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003573 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003574
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003575 cl.RunHook(
3576 committing=not options.upload,
3577 may_prompt=False,
3578 verbose=options.verbose,
3579 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003580 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003581
3582
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003583def GenerateGerritChangeId(message):
3584 """Returns Ixxxxxx...xxx change id.
3585
3586 Works the same way as
3587 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3588 but can be called on demand on all platforms.
3589
3590 The basic idea is to generate git hash of a state of the tree, original commit
3591 message, author/committer info and timestamps.
3592 """
3593 lines = []
3594 tree_hash = RunGitSilent(['write-tree'])
3595 lines.append('tree %s' % tree_hash.strip())
3596 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3597 if code == 0:
3598 lines.append('parent %s' % parent.strip())
3599 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3600 lines.append('author %s' % author.strip())
3601 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3602 lines.append('committer %s' % committer.strip())
3603 lines.append('')
3604 # Note: Gerrit's commit-hook actually cleans message of some lines and
3605 # whitespace. This code is not doing this, but it clearly won't decrease
3606 # entropy.
3607 lines.append(message)
3608 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3609 stdin='\n'.join(lines))
3610 return 'I%s' % change_hash.strip()
3611
3612
wittman@chromium.org455dc922015-01-26 20:15:50 +00003613def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3614 """Computes the remote branch ref to use for the CL.
3615
3616 Args:
3617 remote (str): The git remote for the CL.
3618 remote_branch (str): The git remote branch for the CL.
3619 target_branch (str): The target branch specified by the user.
3620 pending_prefix (str): The pending prefix from the settings.
3621 """
3622 if not (remote and remote_branch):
3623 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003624
wittman@chromium.org455dc922015-01-26 20:15:50 +00003625 if target_branch:
3626 # Cannonicalize branch references to the equivalent local full symbolic
3627 # refs, which are then translated into the remote full symbolic refs
3628 # below.
3629 if '/' not in target_branch:
3630 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3631 else:
3632 prefix_replacements = (
3633 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3634 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3635 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3636 )
3637 match = None
3638 for regex, replacement in prefix_replacements:
3639 match = re.search(regex, target_branch)
3640 if match:
3641 remote_branch = target_branch.replace(match.group(0), replacement)
3642 break
3643 if not match:
3644 # This is a branch path but not one we recognize; use as-is.
3645 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003646 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3647 # Handle the refs that need to land in different refs.
3648 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003649
wittman@chromium.org455dc922015-01-26 20:15:50 +00003650 # Create the true path to the remote branch.
3651 # Does the following translation:
3652 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3653 # * refs/remotes/origin/master -> refs/heads/master
3654 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3655 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3656 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3657 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3658 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3659 'refs/heads/')
3660 elif remote_branch.startswith('refs/remotes/branch-heads'):
3661 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3662 # If a pending prefix exists then replace refs/ with it.
3663 if pending_prefix:
3664 remote_branch = remote_branch.replace('refs/', pending_prefix)
3665 return remote_branch
3666
3667
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003668def cleanup_list(l):
3669 """Fixes a list so that comma separated items are put as individual items.
3670
3671 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3672 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3673 """
3674 items = sum((i.split(',') for i in l), [])
3675 stripped_items = (i.strip() for i in items)
3676 return sorted(filter(None, stripped_items))
3677
3678
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003679@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003680def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003681 """Uploads the current changelist to codereview.
3682
3683 Can skip dependency patchset uploads for a branch by running:
3684 git config branch.branch_name.skip-deps-uploads True
3685 To unset run:
3686 git config --unset branch.branch_name.skip-deps-uploads
3687 Can also set the above globally by using the --global flag.
3688 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003689 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3690 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003691 parser.add_option('--bypass-watchlists', action='store_true',
3692 dest='bypass_watchlists',
3693 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003694 parser.add_option('-f', action='store_true', dest='force',
3695 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003696 parser.add_option('-m', dest='message', help='message for patchset')
tandriib80458a2016-06-23 12:20:07 -07003697 parser.add_option('--message-file', dest='message_file',
3698 help='file which contains message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003699 parser.add_option('-t', dest='title',
3700 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003701 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003702 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003703 help='reviewer email addresses')
3704 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003705 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003706 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003707 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003708 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003709 parser.add_option('--emulate_svn_auto_props',
3710 '--emulate-svn-auto-props',
3711 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003712 dest="emulate_svn_auto_props",
3713 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003714 parser.add_option('-c', '--use-commit-queue', action='store_true',
3715 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003716 parser.add_option('--private', action='store_true',
3717 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003718 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003719 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003720 metavar='TARGET',
3721 help='Apply CL to remote ref TARGET. ' +
3722 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003723 parser.add_option('--squash', action='store_true',
3724 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003725 parser.add_option('--no-squash', action='store_true',
3726 help='Don\'t squash multiple commits into one ' +
3727 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003728 parser.add_option('--email', default=None,
3729 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003730 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3731 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003732 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3733 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003734 help='Send the patchset to do a CQ dry run right after '
3735 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003736 parser.add_option('--dependencies', action='store_true',
3737 help='Uploads CLs of all the local branches that depend on '
3738 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003739
rmistry@google.com2dd99862015-06-22 12:22:18 +00003740 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003741 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003742 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003743 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003744 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003745 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003746 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003747
sbc@chromium.org71437c02015-04-09 19:29:40 +00003748 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003749 return 1
3750
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003751 options.reviewers = cleanup_list(options.reviewers)
3752 options.cc = cleanup_list(options.cc)
3753
tandriib80458a2016-06-23 12:20:07 -07003754 if options.message_file:
3755 if options.message:
3756 parser.error('only one of --message and --message-file allowed.')
3757 options.message = gclient_utils.FileRead(options.message_file)
3758 options.message_file = None
3759
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003760 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3761 settings.GetIsGerrit()
3762
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003763 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00003764 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003765
3766
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003767def IsSubmoduleMergeCommit(ref):
3768 # When submodules are added to the repo, we expect there to be a single
3769 # non-git-svn merge commit at remote HEAD with a signature comment.
3770 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003771 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003772 return RunGit(cmd) != ''
3773
3774
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003775def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003776 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003777
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003778 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3779 upstream and closes the issue automatically and atomically.
3780
3781 Otherwise (in case of Rietveld):
3782 Squashes branch into a single commit.
3783 Updates changelog with metadata (e.g. pointer to review).
3784 Pushes/dcommits the code upstream.
3785 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003786 """
3787 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3788 help='bypass upload presubmit hook')
3789 parser.add_option('-m', dest='message',
3790 help="override review description")
3791 parser.add_option('-f', action='store_true', dest='force',
3792 help="force yes to questions (don't prompt)")
3793 parser.add_option('-c', dest='contributor',
3794 help="external contributor for patch (appended to " +
3795 "description and used as author for git). Should be " +
3796 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003797 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003798 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003799 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003800 auth_config = auth.extract_auth_config_from_options(options)
3801
3802 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003803
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003804 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3805 if cl.IsGerrit():
3806 if options.message:
3807 # This could be implemented, but it requires sending a new patch to
3808 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3809 # Besides, Gerrit has the ability to change the commit message on submit
3810 # automatically, thus there is no need to support this option (so far?).
3811 parser.error('-m MESSAGE option is not supported for Gerrit.')
3812 if options.contributor:
3813 parser.error(
3814 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3815 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3816 'the contributor\'s "name <email>". If you can\'t upload such a '
3817 'commit for review, contact your repository admin and request'
3818 '"Forge-Author" permission.')
3819 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3820 options.verbose)
3821
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003822 current = cl.GetBranch()
3823 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3824 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07003825 print()
3826 print('Attempting to push branch %r into another local branch!' % current)
3827 print()
3828 print('Either reparent this branch on top of origin/master:')
3829 print(' git reparent-branch --root')
3830 print()
3831 print('OR run `git rebase-update` if you think the parent branch is ')
3832 print('already committed.')
3833 print()
3834 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003835 return 1
3836
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003837 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003838 # Default to merging against our best guess of the upstream branch.
3839 args = [cl.GetUpstreamBranch()]
3840
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003841 if options.contributor:
3842 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07003843 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003844 return 1
3845
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003846 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003847 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003848
sbc@chromium.org71437c02015-04-09 19:29:40 +00003849 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003850 return 1
3851
3852 # This rev-list syntax means "show all commits not in my branch that
3853 # are in base_branch".
3854 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3855 base_branch]).splitlines()
3856 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07003857 print('Base branch "%s" has %d commits '
3858 'not in this branch.' % (base_branch, len(upstream_commits)))
3859 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003860 return 1
3861
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003862 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003863 svn_head = None
3864 if cmd == 'dcommit' or base_has_submodules:
3865 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
3866 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003867
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003868 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003869 # If the base_head is a submodule merge commit, the first parent of the
3870 # base_head should be a git-svn commit, which is what we're interested in.
3871 base_svn_head = base_branch
3872 if base_has_submodules:
3873 base_svn_head += '^1'
3874
3875 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003876 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07003877 print('This branch has %d additional commits not upstreamed yet.'
3878 % len(extra_commits.splitlines()))
3879 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
3880 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003881 return 1
3882
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003883 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003884 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003885 author = None
3886 if options.contributor:
3887 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003888 hook_results = cl.RunHook(
3889 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003890 may_prompt=not options.force,
3891 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003892 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003893 if not hook_results.should_continue():
3894 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003895
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003896 # Check the tree status if the tree status URL is set.
3897 status = GetTreeStatus()
3898 if 'closed' == status:
3899 print('The tree is closed. Please wait for it to reopen. Use '
3900 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3901 return 1
3902 elif 'unknown' == status:
3903 print('Unable to determine tree status. Please verify manually and '
3904 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3905 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003906
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003907 change_desc = ChangeDescription(options.message)
3908 if not change_desc.description and cl.GetIssue():
3909 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003910
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003911 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00003912 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003913 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00003914 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003915 print('No description set.')
3916 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00003917 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003918
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003919 # Keep a separate copy for the commit message, because the commit message
3920 # contains the link to the Rietveld issue, while the Rietveld message contains
3921 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003922 # Keep a separate copy for the commit message.
3923 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00003924 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003925
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003926 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00003927 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00003928 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00003929 # after it. Add a period on a new line to circumvent this. Also add a space
3930 # before the period to make sure that Gitiles continues to correctly resolve
3931 # the URL.
3932 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003933 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003934 commit_desc.append_footer('Patch from %s.' % options.contributor)
3935
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00003936 print('Description:')
3937 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003938
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003939 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003940 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00003941 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003942
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003943 # We want to squash all this branch's commits into one commit with the proper
3944 # description. We do this by doing a "reset --soft" to the base branch (which
3945 # keeps the working copy the same), then dcommitting that. If origin/master
3946 # has a submodule merge commit, we'll also need to cherry-pick the squashed
3947 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003948 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003949 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
3950 # Delete the branches if they exist.
3951 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
3952 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
3953 result = RunGitWithCode(showref_cmd)
3954 if result[0] == 0:
3955 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003956
3957 # We might be in a directory that's present in this branch but not in the
3958 # trunk. Move up to the top of the tree so that git commands that expect a
3959 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003960 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003961 if rel_base_path:
3962 os.chdir(rel_base_path)
3963
3964 # Stuff our change into the merge branch.
3965 # We wrap in a try...finally block so if anything goes wrong,
3966 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003967 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003968 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003969 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003970 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003971 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00003972 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003973 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003974 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003975 RunGit(
3976 [
3977 'commit', '--author', options.contributor,
3978 '-m', commit_desc.description,
3979 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003980 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003981 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003982 if base_has_submodules:
3983 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
3984 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
3985 RunGit(['checkout', CHERRY_PICK_BRANCH])
3986 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003987 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003988 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003989 mirror = settings.GetGitMirror(remote)
3990 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003991 pending_prefix = settings.GetPendingRefPrefix()
3992 if not pending_prefix or branch.startswith(pending_prefix):
3993 # If not using refs/pending/heads/* at all, or target ref is already set
3994 # to pending, then push to the target ref directly.
3995 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003996 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003997 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003998 else:
3999 # Cherry-pick the change on top of pending ref and then push it.
4000 assert branch.startswith('refs/'), branch
4001 assert pending_prefix[-1] == '/', pending_prefix
4002 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004003 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004004 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004005 if retcode == 0:
4006 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004007 else:
4008 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004009 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00004010 'svn', 'dcommit',
4011 '-C%s' % options.similarity,
4012 '--no-rebase', '--rmdir',
4013 ]
4014 if settings.GetForceHttpsCommitUrl():
4015 # Allow forcing https commit URLs for some projects that don't allow
4016 # committing to http URLs (like Google Code).
4017 remote_url = cl.GetGitSvnRemoteUrl()
4018 if urlparse.urlparse(remote_url).scheme == 'http':
4019 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004020 cmd_args.append('--commit-url=%s' % remote_url)
4021 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004022 if 'Committed r' in output:
4023 revision = re.match(
4024 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4025 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004026 finally:
4027 # And then swap back to the original branch and clean up.
4028 RunGit(['checkout', '-q', cl.GetBranch()])
4029 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004030 if base_has_submodules:
4031 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004032
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004033 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004034 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004035 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004036
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004037 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004038 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004039 try:
4040 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4041 # We set pushed_to_pending to False, since it made it all the way to the
4042 # real ref.
4043 pushed_to_pending = False
4044 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004045 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004046
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004047 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004048 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004049 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004050 if not to_pending:
4051 if viewvc_url and revision:
4052 change_desc.append_footer(
4053 'Committed: %s%s' % (viewvc_url, revision))
4054 elif revision:
4055 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004056 print('Closing issue '
4057 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004058 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004059 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004060 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004061 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004062 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004063 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004064 if options.bypass_hooks:
4065 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4066 else:
4067 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004068 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004069
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004070 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004071 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004072 print('The commit is in the pending queue (%s).' % pending_ref)
4073 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4074 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004075
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004076 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4077 if os.path.isfile(hook):
4078 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004079
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004080 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004081
4082
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004083def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004084 print()
4085 print('Waiting for commit to be landed on %s...' % real_ref)
4086 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004087 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4088 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004089 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004090
4091 loop = 0
4092 while True:
4093 sys.stdout.write('fetching (%d)... \r' % loop)
4094 sys.stdout.flush()
4095 loop += 1
4096
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004097 if mirror:
4098 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004099 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4100 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4101 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4102 for commit in commits.splitlines():
4103 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004104 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004105 return commit
4106
4107 current_rev = to_rev
4108
4109
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004110def PushToGitPending(remote, pending_ref, upstream_ref):
4111 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4112
4113 Returns:
4114 (retcode of last operation, output log of last operation).
4115 """
4116 assert pending_ref.startswith('refs/'), pending_ref
4117 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4118 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4119 code = 0
4120 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004121 max_attempts = 3
4122 attempts_left = max_attempts
4123 while attempts_left:
4124 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004125 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004126 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004127
4128 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004129 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004130 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004131 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004132 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004133 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004134 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004135 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004136 continue
4137
4138 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004139 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004140 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004141 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004142 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004143 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4144 'the following files have merge conflicts:' % pending_ref)
4145 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4146 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004147 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004148 return code, out
4149
4150 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004151 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004152 code, out = RunGitWithCode(
4153 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4154 if code == 0:
4155 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004156 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004157 return code, out
4158
vapiera7fbd5a2016-06-16 09:17:49 -07004159 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004160 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004161 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004162 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004163 print('Fatal push error. Make sure your .netrc credentials and git '
4164 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004165 return code, out
4166
vapiera7fbd5a2016-06-16 09:17:49 -07004167 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004168 return code, out
4169
4170
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004171def IsFatalPushFailure(push_stdout):
4172 """True if retrying push won't help."""
4173 return '(prohibited by Gerrit)' in push_stdout
4174
4175
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004176@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004177def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004178 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004179 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004180 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004181 # If it looks like previous commits were mirrored with git-svn.
4182 message = """This repository appears to be a git-svn mirror, but no
4183upstream SVN master is set. You probably need to run 'git auto-svn' once."""
4184 else:
4185 message = """This doesn't appear to be an SVN repository.
4186If your project has a true, writeable git repository, you probably want to run
4187'git cl land' instead.
4188If your project has a git mirror of an upstream SVN master, you probably need
4189to run 'git svn init'.
4190
4191Using the wrong command might cause your commit to appear to succeed, and the
4192review to be closed, without actually landing upstream. If you choose to
4193proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004194 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004195 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
tandrii3bb82ff2016-06-17 07:36:36 -07004196 # TODO(tandrii): kill this post SVN migration with
4197 # https://codereview.chromium.org/2076683002
4198 print('WARNING: chrome infrastructure is migrating SVN repos to Git.\n'
4199 'Please let us know of this project you are committing to:'
4200 ' http://crbug.com/600451')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004201 return SendUpstream(parser, args, 'dcommit')
4202
4203
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004204@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004205def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004206 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004207 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004208 print('This appears to be an SVN repository.')
4209 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004210 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004211 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004212 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004213
4214
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004215@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004216def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004217 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004218 parser.add_option('-b', dest='newbranch',
4219 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004220 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004221 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004222 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4223 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004224 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004225 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004226 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004227 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004228 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004229 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004230
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004231
4232 group = optparse.OptionGroup(
4233 parser,
4234 'Options for continuing work on the current issue uploaded from a '
4235 'different clone (e.g. different machine). Must be used independently '
4236 'from the other options. No issue number should be specified, and the '
4237 'branch must have an issue number associated with it')
4238 group.add_option('--reapply', action='store_true', dest='reapply',
4239 help='Reset the branch and reapply the issue.\n'
4240 'CAUTION: This will undo any local changes in this '
4241 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004242
4243 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004244 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004245 parser.add_option_group(group)
4246
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004247 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004248 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004249 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004250 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004251 auth_config = auth.extract_auth_config_from_options(options)
4252
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004253
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004254 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004255 if options.newbranch:
4256 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004257 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004258 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004259
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004260 cl = Changelist(auth_config=auth_config,
4261 codereview=options.forced_codereview)
4262 if not cl.GetIssue():
4263 parser.error('current branch must have an associated issue')
4264
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004265 upstream = cl.GetUpstreamBranch()
4266 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004267 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004268
4269 RunGit(['reset', '--hard', upstream])
4270 if options.pull:
4271 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004272
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004273 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4274 options.directory)
4275
4276 if len(args) != 1 or not args[0]:
4277 parser.error('Must specify issue number or url')
4278
4279 # We don't want uncommitted changes mixed up with the patch.
4280 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004281 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004282
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004283 if options.newbranch:
4284 if options.force:
4285 RunGit(['branch', '-D', options.newbranch],
4286 stderr=subprocess2.PIPE, error_ok=True)
4287 RunGit(['new-branch', options.newbranch])
4288
4289 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4290
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004291 if cl.IsGerrit():
4292 if options.reject:
4293 parser.error('--reject is not supported with Gerrit codereview.')
4294 if options.nocommit:
4295 parser.error('--nocommit is not supported with Gerrit codereview.')
4296 if options.directory:
4297 parser.error('--directory is not supported with Gerrit codereview.')
4298
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004299 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004300 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004301
4302
4303def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004304 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004305 # Provide a wrapper for git svn rebase to help avoid accidental
4306 # git svn dcommit.
4307 # It's the only command that doesn't use parser at all since we just defer
4308 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004309
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004310 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004311
4312
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004313def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004314 """Fetches the tree status and returns either 'open', 'closed',
4315 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004316 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004317 if url:
4318 status = urllib2.urlopen(url).read().lower()
4319 if status.find('closed') != -1 or status == '0':
4320 return 'closed'
4321 elif status.find('open') != -1 or status == '1':
4322 return 'open'
4323 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004324 return 'unset'
4325
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004326
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004327def GetTreeStatusReason():
4328 """Fetches the tree status from a json url and returns the message
4329 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004330 url = settings.GetTreeStatusUrl()
4331 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004332 connection = urllib2.urlopen(json_url)
4333 status = json.loads(connection.read())
4334 connection.close()
4335 return status['message']
4336
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004337
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004338def GetBuilderMaster(bot_list):
4339 """For a given builder, fetch the master from AE if available."""
4340 map_url = 'https://builders-map.appspot.com/'
4341 try:
4342 master_map = json.load(urllib2.urlopen(map_url))
4343 except urllib2.URLError as e:
4344 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4345 (map_url, e))
4346 except ValueError as e:
4347 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4348 if not master_map:
4349 return None, 'Failed to build master map.'
4350
4351 result_master = ''
4352 for bot in bot_list:
4353 builder = bot.split(':', 1)[0]
4354 master_list = master_map.get(builder, [])
4355 if not master_list:
4356 return None, ('No matching master for builder %s.' % builder)
4357 elif len(master_list) > 1:
4358 return None, ('The builder name %s exists in multiple masters %s.' %
4359 (builder, master_list))
4360 else:
4361 cur_master = master_list[0]
4362 if not result_master:
4363 result_master = cur_master
4364 elif result_master != cur_master:
4365 return None, 'The builders do not belong to the same master.'
4366 return result_master, None
4367
4368
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004369def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004370 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004371 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004372 status = GetTreeStatus()
4373 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004374 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004375 return 2
4376
vapiera7fbd5a2016-06-16 09:17:49 -07004377 print('The tree is %s' % status)
4378 print()
4379 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004380 if status != 'open':
4381 return 1
4382 return 0
4383
4384
maruel@chromium.org15192402012-09-06 12:38:29 +00004385def CMDtry(parser, args):
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004386 """Triggers try jobs through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00004387 group = optparse.OptionGroup(parser, "Try job options")
4388 group.add_option(
4389 "-b", "--bot", action="append",
4390 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
4391 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004392 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00004393 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004394 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00004395 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004396 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00004397 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004398 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004399 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004400 "-r", "--revision",
4401 help="Revision to use for the try job; default: the "
4402 "revision will be determined by the try server; see "
4403 "its waterfall for more info")
4404 group.add_option(
4405 "-c", "--clobber", action="store_true", default=False,
4406 help="Force a clobber before building; e.g. don't do an "
4407 "incremental build")
4408 group.add_option(
4409 "--project",
4410 help="Override which project to use. Projects are defined "
4411 "server-side to define what default bot set to use")
4412 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004413 "-p", "--property", dest="properties", action="append", default=[],
4414 help="Specify generic properties in the form -p key1=value1 -p "
4415 "key2=value2 etc (buildbucket only). The value will be treated as "
4416 "json if decodable, or as string otherwise.")
4417 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004418 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004419 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004420 "--use-rietveld", action="store_true", default=False,
4421 help="Use Rietveld to trigger try jobs.")
4422 group.add_option(
4423 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4424 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004425 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004426 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004427 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004428 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004429
machenbach@chromium.org45453142015-09-15 08:45:22 +00004430 if options.use_rietveld and options.properties:
4431 parser.error('Properties can only be specified with buildbucket')
4432
4433 # Make sure that all properties are prop=value pairs.
4434 bad_params = [x for x in options.properties if '=' not in x]
4435 if bad_params:
4436 parser.error('Got properties with missing "=": %s' % bad_params)
4437
maruel@chromium.org15192402012-09-06 12:38:29 +00004438 if args:
4439 parser.error('Unknown arguments: %s' % args)
4440
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004441 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004442 if not cl.GetIssue():
4443 parser.error('Need to upload first')
4444
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004445 if cl.IsGerrit():
4446 parser.error(
4447 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4448 'If your project has Commit Queue, dry run is a workaround:\n'
4449 ' git cl set-commit --dry-run')
4450 # Code below assumes Rietveld issue.
4451 # TODO(tandrii): actually implement for Gerrit http://crbug.com/599931.
4452
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004453 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004454 if props.get('closed'):
4455 parser.error('Cannot send tryjobs for a closed CL')
4456
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004457 if props.get('private'):
4458 parser.error('Cannot use trybots with private issue')
4459
maruel@chromium.org15192402012-09-06 12:38:29 +00004460 if not options.name:
4461 options.name = cl.GetBranch()
4462
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004463 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004464 options.master, err_msg = GetBuilderMaster(options.bot)
4465 if err_msg:
4466 parser.error('Tryserver master cannot be found because: %s\n'
4467 'Please manually specify the tryserver master'
4468 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004469
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004470 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004471 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004472 if not options.bot:
4473 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004474
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004475 # Get try masters from PRESUBMIT.py files.
4476 masters = presubmit_support.DoGetTryMasters(
4477 change,
4478 change.LocalPaths(),
4479 settings.GetRoot(),
4480 None,
4481 None,
4482 options.verbose,
4483 sys.stdout)
4484 if masters:
4485 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004486
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004487 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4488 options.bot = presubmit_support.DoGetTrySlaves(
4489 change,
4490 change.LocalPaths(),
4491 settings.GetRoot(),
4492 None,
4493 None,
4494 options.verbose,
4495 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004496
4497 if not options.bot:
4498 # Get try masters from cq.cfg if any.
4499 # TODO(tandrii): some (but very few) projects store cq.cfg in different
4500 # location.
4501 cq_cfg = os.path.join(change.RepositoryRoot(),
4502 'infra', 'config', 'cq.cfg')
4503 if os.path.exists(cq_cfg):
4504 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00004505 cq_masters = commit_queue.get_master_builder_map(
4506 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004507 for master, builders in cq_masters.iteritems():
4508 for builder in builders:
4509 # Skip presubmit builders, because these will fail without LGTM.
machenbach@chromium.org2403e802016-04-29 12:34:42 +00004510 masters.setdefault(master, {})[builder] = ['defaulttests']
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004511 if masters:
tandriib93dd2b2016-06-07 08:03:08 -07004512 print('Loaded default bots from CQ config (%s)' % cq_cfg)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004513 return masters
tandriib93dd2b2016-06-07 08:03:08 -07004514 else:
4515 print('CQ config exists (%s) but has no try bots listed' % cq_cfg)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004516
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004517 if not options.bot:
4518 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00004519
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004520 builders_and_tests = {}
4521 # TODO(machenbach): The old style command-line options don't support
4522 # multiple try masters yet.
4523 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4524 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4525
4526 for bot in old_style:
4527 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004528 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004529 elif ',' in bot:
4530 parser.error('Specify one bot per --bot flag')
4531 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004532 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004533
4534 for bot, tests in new_style:
4535 builders_and_tests.setdefault(bot, []).extend(tests)
4536
4537 # Return a master map with one master to be backwards compatible. The
4538 # master name defaults to an empty string, which will cause the master
4539 # not to be set on rietveld (deprecated).
4540 return {options.master: builders_and_tests}
4541
4542 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004543
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004544 for builders in masters.itervalues():
4545 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004546 print('ERROR You are trying to send a job to a triggered bot. This type '
4547 'of bot requires an\ninitial job from a parent (usually a builder).'
4548 ' Instead send your job to the parent.\n'
4549 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004550 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004551
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004552 patchset = cl.GetMostRecentPatchset()
4553 if patchset and patchset != cl.GetPatchset():
4554 print(
4555 '\nWARNING Mismatch between local config and server. Did a previous '
4556 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4557 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004558 if options.luci:
4559 trigger_luci_job(cl, masters, options)
4560 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004561 try:
4562 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4563 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004564 print('ERROR: %s' % ex)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004565 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004566 except Exception as e:
4567 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
vapiera7fbd5a2016-06-16 09:17:49 -07004568 print('ERROR: Exception when trying to trigger tryjobs: %s\n%s' %
4569 (e, stacktrace))
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004570 return 1
4571 else:
4572 try:
4573 cl.RpcServer().trigger_distributed_try_jobs(
4574 cl.GetIssue(), patchset, options.name, options.clobber,
4575 options.revision, masters)
4576 except urllib2.HTTPError as e:
4577 if e.code == 404:
4578 print('404 from rietveld; '
4579 'did you mean to use "git try" instead of "git cl try"?')
4580 return 1
4581 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004582
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004583 for (master, builders) in sorted(masters.iteritems()):
4584 if master:
vapiera7fbd5a2016-06-16 09:17:49 -07004585 print('Master: %s' % master)
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004586 length = max(len(builder) for builder in builders)
4587 for builder in sorted(builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004588 print(' %*s: %s' % (length, builder, ','.join(builders[builder])))
maruel@chromium.org15192402012-09-06 12:38:29 +00004589 return 0
4590
4591
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004592def CMDtry_results(parser, args):
4593 group = optparse.OptionGroup(parser, "Try job results options")
4594 group.add_option(
4595 "-p", "--patchset", type=int, help="patchset number if not current.")
4596 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004597 "--print-master", action='store_true', help="print master name as well.")
4598 group.add_option(
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004599 "--color", action='store_true', default=setup_color.IS_TTY,
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004600 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004601 group.add_option(
4602 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4603 help="Host of buildbucket. The default host is %default.")
4604 parser.add_option_group(group)
4605 auth.add_auth_options(parser)
4606 options, args = parser.parse_args(args)
4607 if args:
4608 parser.error('Unrecognized args: %s' % ' '.join(args))
4609
4610 auth_config = auth.extract_auth_config_from_options(options)
4611 cl = Changelist(auth_config=auth_config)
4612 if not cl.GetIssue():
4613 parser.error('Need to upload first')
4614
4615 if not options.patchset:
4616 options.patchset = cl.GetMostRecentPatchset()
4617 if options.patchset and options.patchset != cl.GetPatchset():
4618 print(
4619 '\nWARNING Mismatch between local config and server. Did a previous '
4620 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4621 'Continuing using\npatchset %s.\n' % options.patchset)
4622 try:
4623 jobs = fetch_try_jobs(auth_config, cl, options)
4624 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004625 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004626 return 1
4627 except Exception as e:
4628 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
vapiera7fbd5a2016-06-16 09:17:49 -07004629 print('ERROR: Exception when trying to fetch tryjobs: %s\n%s' %
4630 (e, stacktrace))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004631 return 1
4632 print_tryjobs(options, jobs)
4633 return 0
4634
4635
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004636@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004637def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004638 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004639 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004640 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004641 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004642
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004643 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004644 if args:
4645 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004646 branch = cl.GetBranch()
4647 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004648 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004649 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004650
4651 # Clear configured merge-base, if there is one.
4652 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004653 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004654 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004655 return 0
4656
4657
thestig@chromium.org00858c82013-12-02 23:08:03 +00004658def CMDweb(parser, args):
4659 """Opens the current CL in the web browser."""
4660 _, args = parser.parse_args(args)
4661 if args:
4662 parser.error('Unrecognized args: %s' % ' '.join(args))
4663
4664 issue_url = Changelist().GetIssueURL()
4665 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004666 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004667 return 1
4668
4669 webbrowser.open(issue_url)
4670 return 0
4671
4672
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004673def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004674 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004675 parser.add_option('-d', '--dry-run', action='store_true',
4676 help='trigger in dry run mode')
4677 parser.add_option('-c', '--clear', action='store_true',
4678 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004679 auth.add_auth_options(parser)
4680 options, args = parser.parse_args(args)
4681 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004682 if args:
4683 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004684 if options.dry_run and options.clear:
4685 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4686
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004687 cl = Changelist(auth_config=auth_config)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004688 if options.clear:
4689 state = _CQState.CLEAR
4690 elif options.dry_run:
4691 state = _CQState.DRY_RUN
4692 else:
4693 state = _CQState.COMMIT
4694 if not cl.GetIssue():
4695 parser.error('Must upload the issue first')
4696 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004697 return 0
4698
4699
groby@chromium.org411034a2013-02-26 15:12:01 +00004700def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004701 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004702 auth.add_auth_options(parser)
4703 options, args = parser.parse_args(args)
4704 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004705 if args:
4706 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004707 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00004708 # Ensure there actually is an issue to close.
4709 cl.GetDescription()
4710 cl.CloseIssue()
4711 return 0
4712
4713
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004714def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004715 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004716 auth.add_auth_options(parser)
4717 options, args = parser.parse_args(args)
4718 auth_config = auth.extract_auth_config_from_options(options)
4719 if args:
4720 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004721
4722 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004723 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004724 # Staged changes would be committed along with the patch from last
4725 # upload, hence counted toward the "last upload" side in the final
4726 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004727 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004728 return 1
4729
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004730 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004731 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004732 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004733 if not issue:
4734 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004735 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004736 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004737
4738 # Create a new branch based on the merge-base
4739 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004740 # Clear cached branch in cl object, to avoid overwriting original CL branch
4741 # properties.
4742 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004743 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004744 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004745 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004746 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004747 return rtn
4748
wychen@chromium.org06928532015-02-03 02:11:29 +00004749 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004750 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00004751 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004752 finally:
4753 RunGit(['checkout', '-q', branch])
4754 RunGit(['branch', '-D', TMP_BRANCH])
4755
4756 return 0
4757
4758
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004759def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004760 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004761 parser.add_option(
4762 '--no-color',
4763 action='store_true',
4764 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004765 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004766 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004767 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004768
4769 author = RunGit(['config', 'user.email']).strip() or None
4770
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004771 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004772
4773 if args:
4774 if len(args) > 1:
4775 parser.error('Unknown args')
4776 base_branch = args[0]
4777 else:
4778 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004779 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004780
4781 change = cl.GetChange(base_branch, None)
4782 return owners_finder.OwnersFinder(
4783 [f.LocalPath() for f in
4784 cl.GetChange(base_branch, None).AffectedFiles()],
4785 change.RepositoryRoot(), author,
4786 fopen=file, os_path=os.path, glob=glob.glob,
4787 disable_color=options.no_color).run()
4788
4789
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004790def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004791 """Generates a diff command."""
4792 # Generate diff for the current branch's changes.
4793 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4794 upstream_commit, '--' ]
4795
4796 if args:
4797 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004798 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004799 diff_cmd.append(arg)
4800 else:
4801 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004802
4803 return diff_cmd
4804
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004805def MatchingFileType(file_name, extensions):
4806 """Returns true if the file name ends with one of the given extensions."""
4807 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004808
enne@chromium.org555cfe42014-01-29 18:21:39 +00004809@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004810def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004811 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00004812 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07004813 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004814 parser.add_option('--full', action='store_true',
4815 help='Reformat the full content of all touched files')
4816 parser.add_option('--dry-run', action='store_true',
4817 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004818 parser.add_option('--python', action='store_true',
4819 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004820 parser.add_option('--diff', action='store_true',
4821 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004822 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004823
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004824 # git diff generates paths against the root of the repository. Change
4825 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004826 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004827 if rel_base_path:
4828 os.chdir(rel_base_path)
4829
digit@chromium.org29e47272013-05-17 17:01:46 +00004830 # Grab the merge-base commit, i.e. the upstream commit of the current
4831 # branch when it was created or the last time it was rebased. This is
4832 # to cover the case where the user may have called "git fetch origin",
4833 # moving the origin branch to a newer commit, but hasn't rebased yet.
4834 upstream_commit = None
4835 cl = Changelist()
4836 upstream_branch = cl.GetUpstreamBranch()
4837 if upstream_branch:
4838 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4839 upstream_commit = upstream_commit.strip()
4840
4841 if not upstream_commit:
4842 DieWithError('Could not find base commit for this branch. '
4843 'Are you in detached state?')
4844
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004845 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4846 diff_output = RunGit(changed_files_cmd)
4847 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004848 # Filter out files deleted by this CL
4849 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004850
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004851 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4852 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4853 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004854 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004855
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004856 top_dir = os.path.normpath(
4857 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
4858
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004859 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4860 # formatted. This is used to block during the presubmit.
4861 return_value = 0
4862
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004863 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004864 # Locate the clang-format binary in the checkout
4865 try:
4866 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07004867 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004868 DieWithError(e)
4869
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004870 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004871 cmd = [clang_format_tool]
4872 if not opts.dry_run and not opts.diff:
4873 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004874 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004875 if opts.diff:
4876 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004877 else:
4878 env = os.environ.copy()
4879 env['PATH'] = str(os.path.dirname(clang_format_tool))
4880 try:
4881 script = clang_format.FindClangFormatScriptInChromiumTree(
4882 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07004883 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004884 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004885
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004886 cmd = [sys.executable, script, '-p0']
4887 if not opts.dry_run and not opts.diff:
4888 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004889
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004890 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
4891 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004892
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004893 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
4894 if opts.diff:
4895 sys.stdout.write(stdout)
4896 if opts.dry_run and len(stdout) > 0:
4897 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004898
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004899 # Similar code to above, but using yapf on .py files rather than clang-format
4900 # on C/C++ files
4901 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004902 yapf_tool = gclient_utils.FindExecutable('yapf')
4903 if yapf_tool is None:
4904 DieWithError('yapf not found in PATH')
4905
4906 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004907 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004908 cmd = [yapf_tool]
4909 if not opts.dry_run and not opts.diff:
4910 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004911 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004912 if opts.diff:
4913 sys.stdout.write(stdout)
4914 else:
4915 # TODO(sbc): yapf --lines mode still has some issues.
4916 # https://github.com/google/yapf/issues/154
4917 DieWithError('--python currently only works with --full')
4918
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004919 # Dart's formatter does not have the nice property of only operating on
4920 # modified chunks, so hard code full.
4921 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004922 try:
4923 command = [dart_format.FindDartFmtToolInChromiumTree()]
4924 if not opts.dry_run and not opts.diff:
4925 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004926 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004927
ppi@chromium.org6593d932016-03-03 15:41:15 +00004928 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004929 if opts.dry_run and stdout:
4930 return_value = 2
4931 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07004932 print('Warning: Unable to check Dart code formatting. Dart SDK not '
4933 'found in this checkout. Files in other languages are still '
4934 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004935
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004936 # Format GN build files. Always run on full build files for canonical form.
4937 if gn_diff_files:
4938 cmd = ['gn', 'format']
4939 if not opts.dry_run and not opts.diff:
4940 cmd.append('--in-place')
4941 for gn_diff_file in gn_diff_files:
bsep@chromium.org627d9002016-04-29 00:00:52 +00004942 stdout = RunCommand(cmd + [gn_diff_file],
4943 shell=sys.platform == 'win32',
4944 cwd=top_dir)
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004945 if opts.diff:
4946 sys.stdout.write(stdout)
4947
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004948 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004949
4950
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004951@subcommand.usage('<codereview url or issue id>')
4952def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004953 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004954 _, args = parser.parse_args(args)
4955
4956 if len(args) != 1:
4957 parser.print_help()
4958 return 1
4959
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004960 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00004961 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004962 parser.print_help()
4963 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00004964 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004965
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004966 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00004967 output = RunGit(['config', '--local', '--get-regexp',
4968 r'branch\..*\.%s' % issueprefix],
4969 error_ok=True)
4970 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004971 if issue == target_issue:
4972 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004973
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004974 branches = []
4975 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00004976 branches.extend(find_issues(cls.IssueSettingSuffix()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004977 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07004978 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004979 return 1
4980 if len(branches) == 1:
4981 RunGit(['checkout', branches[0]])
4982 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004983 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004984 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07004985 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004986 which = raw_input('Choose by index: ')
4987 try:
4988 RunGit(['checkout', branches[int(which)]])
4989 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07004990 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004991 return 1
4992
4993 return 0
4994
4995
maruel@chromium.org29404b52014-09-08 22:58:00 +00004996def CMDlol(parser, args):
4997 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07004998 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00004999 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5000 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5001 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005002 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005003 return 0
5004
5005
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005006class OptionParser(optparse.OptionParser):
5007 """Creates the option parse and add --verbose support."""
5008 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005009 optparse.OptionParser.__init__(
5010 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005011 self.add_option(
5012 '-v', '--verbose', action='count', default=0,
5013 help='Use 2 times for more debugging info')
5014
5015 def parse_args(self, args=None, values=None):
5016 options, args = optparse.OptionParser.parse_args(self, args, values)
5017 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5018 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5019 return options, args
5020
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005021
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005022def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005023 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005024 print('\nYour python version %s is unsupported, please upgrade.\n' %
5025 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005026 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005027
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005028 # Reload settings.
5029 global settings
5030 settings = Settings()
5031
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005032 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005033 dispatcher = subcommand.CommandDispatcher(__name__)
5034 try:
5035 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005036 except auth.AuthenticationError as e:
5037 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005038 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005039 if e.code != 500:
5040 raise
5041 DieWithError(
5042 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5043 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005044 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005045
5046
5047if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005048 # These affect sys.stdout so do it outside of main() to simplify mocks in
5049 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005050 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005051 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005052 try:
5053 sys.exit(main(sys.argv[1:]))
5054 except KeyboardInterrupt:
5055 sys.stderr.write('interrupted\n')
5056 sys.exit(1)