blob: 66415668adaa7ab6823af7865fed8c80a636c28c [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:
tandrii4d0545a2016-07-06 03:56:49 -07001421 if options.use_commit_queue:
1422 self.SetCQState(_CQState.COMMIT)
1423 elif options.cq_dry_run:
1424 self.SetCQState(_CQState.DRY_RUN)
1425
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001426 git_set_branch_value('last-upload-hash',
1427 RunGit(['rev-parse', 'HEAD']).strip())
1428 # Run post upload hooks, if specified.
1429 if settings.GetRunPostUploadHook():
1430 presubmit_support.DoPostUploadExecuter(
1431 change,
1432 self,
1433 settings.GetRoot(),
1434 options.verbose,
1435 sys.stdout)
1436
1437 # Upload all dependencies if specified.
1438 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001439 print()
1440 print('--dependencies has been specified.')
1441 print('All dependent local branches will be re-uploaded.')
1442 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001443 # Remove the dependencies flag from args so that we do not end up in a
1444 # loop.
1445 orig_args.remove('--dependencies')
1446 ret = upload_branch_deps(self, orig_args)
1447 return ret
1448
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001449 def SetCQState(self, new_state):
1450 """Update the CQ state for latest patchset.
1451
1452 Issue must have been already uploaded and known.
1453 """
1454 assert new_state in _CQState.ALL_STATES
1455 assert self.GetIssue()
1456 return self._codereview_impl.SetCQState(new_state)
1457
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001458 # Forward methods to codereview specific implementation.
1459
1460 def CloseIssue(self):
1461 return self._codereview_impl.CloseIssue()
1462
1463 def GetStatus(self):
1464 return self._codereview_impl.GetStatus()
1465
1466 def GetCodereviewServer(self):
1467 return self._codereview_impl.GetCodereviewServer()
1468
1469 def GetApprovingReviewers(self):
1470 return self._codereview_impl.GetApprovingReviewers()
1471
1472 def GetMostRecentPatchset(self):
1473 return self._codereview_impl.GetMostRecentPatchset()
1474
1475 def __getattr__(self, attr):
1476 # This is because lots of untested code accesses Rietveld-specific stuff
1477 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001478 # on a case by case basis.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001479 return getattr(self._codereview_impl, attr)
1480
1481
1482class _ChangelistCodereviewBase(object):
1483 """Abstract base class encapsulating codereview specifics of a changelist."""
1484 def __init__(self, changelist):
1485 self._changelist = changelist # instance of Changelist
1486
1487 def __getattr__(self, attr):
1488 # Forward methods to changelist.
1489 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1490 # _RietveldChangelistImpl to avoid this hack?
1491 return getattr(self._changelist, attr)
1492
1493 def GetStatus(self):
1494 """Apply a rough heuristic to give a simple summary of an issue's review
1495 or CQ status, assuming adherence to a common workflow.
1496
1497 Returns None if no issue for this branch, or specific string keywords.
1498 """
1499 raise NotImplementedError()
1500
1501 def GetCodereviewServer(self):
1502 """Returns server URL without end slash, like "https://codereview.com"."""
1503 raise NotImplementedError()
1504
1505 def FetchDescription(self):
1506 """Fetches and returns description from the codereview server."""
1507 raise NotImplementedError()
1508
1509 def GetCodereviewServerSetting(self):
1510 """Returns git config setting for the codereview server."""
1511 raise NotImplementedError()
1512
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001513 @classmethod
1514 def IssueSetting(cls, branch):
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001515 return 'branch.%s.%s' % (branch, cls.IssueSettingSuffix())
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001516
1517 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001518 def IssueSettingSuffix(cls):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001519 """Returns name of git config setting which stores issue number for a given
1520 branch."""
1521 raise NotImplementedError()
1522
1523 def PatchsetSetting(self):
1524 """Returns name of git config setting which stores issue number."""
1525 raise NotImplementedError()
1526
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001527 def _PostUnsetIssueProperties(self):
1528 """Which branch-specific properties to erase when unsettin issue."""
1529 raise NotImplementedError()
1530
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001531 def GetRieveldObjForPresubmit(self):
1532 # This is an unfortunate Rietveld-embeddedness in presubmit.
1533 # For non-Rietveld codereviews, this probably should return a dummy object.
1534 raise NotImplementedError()
1535
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001536 def GetGerritObjForPresubmit(self):
1537 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1538 return None
1539
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001540 def UpdateDescriptionRemote(self, description):
1541 """Update the description on codereview site."""
1542 raise NotImplementedError()
1543
1544 def CloseIssue(self):
1545 """Closes the issue."""
1546 raise NotImplementedError()
1547
1548 def GetApprovingReviewers(self):
1549 """Returns a list of reviewers approving the change.
1550
1551 Note: not necessarily committers.
1552 """
1553 raise NotImplementedError()
1554
1555 def GetMostRecentPatchset(self):
1556 """Returns the most recent patchset number from the codereview site."""
1557 raise NotImplementedError()
1558
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001559 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1560 directory):
1561 """Fetches and applies the issue.
1562
1563 Arguments:
1564 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1565 reject: if True, reject the failed patch instead of switching to 3-way
1566 merge. Rietveld only.
1567 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1568 only.
1569 directory: switch to directory before applying the patch. Rietveld only.
1570 """
1571 raise NotImplementedError()
1572
1573 @staticmethod
1574 def ParseIssueURL(parsed_url):
1575 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1576 failed."""
1577 raise NotImplementedError()
1578
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001579 def EnsureAuthenticated(self, force):
1580 """Best effort check that user is authenticated with codereview server.
1581
1582 Arguments:
1583 force: whether to skip confirmation questions.
1584 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001585 raise NotImplementedError()
1586
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001587 def CMDUploadChange(self, options, args, change):
1588 """Uploads a change to codereview."""
1589 raise NotImplementedError()
1590
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001591 def SetCQState(self, new_state):
1592 """Update the CQ state for latest patchset.
1593
1594 Issue must have been already uploaded and known.
1595 """
1596 raise NotImplementedError()
1597
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001598
1599class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1600 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1601 super(_RietveldChangelistImpl, self).__init__(changelist)
1602 assert settings, 'must be initialized in _ChangelistCodereviewBase'
martiniss6eda05f2016-06-30 10:18:35 -07001603 if not rietveld_server:
1604 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001605
1606 self._rietveld_server = rietveld_server
1607 self._auth_config = auth_config
1608 self._props = None
1609 self._rpc_server = None
1610
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001611 def GetCodereviewServer(self):
1612 if not self._rietveld_server:
1613 # If we're on a branch then get the server potentially associated
1614 # with that branch.
1615 if self.GetIssue():
1616 rietveld_server_setting = self.GetCodereviewServerSetting()
1617 if rietveld_server_setting:
1618 self._rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1619 ['config', rietveld_server_setting], error_ok=True).strip())
1620 if not self._rietveld_server:
1621 self._rietveld_server = settings.GetDefaultServerUrl()
1622 return self._rietveld_server
1623
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001624 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001625 """Best effort check that user is authenticated with Rietveld server."""
1626 if self._auth_config.use_oauth2:
1627 authenticator = auth.get_authenticator_for_host(
1628 self.GetCodereviewServer(), self._auth_config)
1629 if not authenticator.has_cached_credentials():
1630 raise auth.LoginRequiredError(self.GetCodereviewServer())
1631
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001632 def FetchDescription(self):
1633 issue = self.GetIssue()
1634 assert issue
1635 try:
1636 return self.RpcServer().get_description(issue).strip()
1637 except urllib2.HTTPError as e:
1638 if e.code == 404:
1639 DieWithError(
1640 ('\nWhile fetching the description for issue %d, received a '
1641 '404 (not found)\n'
1642 'error. It is likely that you deleted this '
1643 'issue on the server. If this is the\n'
1644 'case, please run\n\n'
1645 ' git cl issue 0\n\n'
1646 'to clear the association with the deleted issue. Then run '
1647 'this command again.') % issue)
1648 else:
1649 DieWithError(
1650 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1651 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001652 print('Warning: Failed to retrieve CL description due to network '
1653 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001654 return ''
1655
1656 def GetMostRecentPatchset(self):
1657 return self.GetIssueProperties()['patchsets'][-1]
1658
1659 def GetPatchSetDiff(self, issue, patchset):
1660 return self.RpcServer().get(
1661 '/download/issue%s_%s.diff' % (issue, patchset))
1662
1663 def GetIssueProperties(self):
1664 if self._props is None:
1665 issue = self.GetIssue()
1666 if not issue:
1667 self._props = {}
1668 else:
1669 self._props = self.RpcServer().get_issue_properties(issue, True)
1670 return self._props
1671
1672 def GetApprovingReviewers(self):
1673 return get_approving_reviewers(self.GetIssueProperties())
1674
1675 def AddComment(self, message):
1676 return self.RpcServer().add_comment(self.GetIssue(), message)
1677
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001678 def GetStatus(self):
1679 """Apply a rough heuristic to give a simple summary of an issue's review
1680 or CQ status, assuming adherence to a common workflow.
1681
1682 Returns None if no issue for this branch, or one of the following keywords:
1683 * 'error' - error from review tool (including deleted issues)
1684 * 'unsent' - not sent for review
1685 * 'waiting' - waiting for review
1686 * 'reply' - waiting for owner to reply to review
1687 * 'lgtm' - LGTM from at least one approved reviewer
1688 * 'commit' - in the commit queue
1689 * 'closed' - closed
1690 """
1691 if not self.GetIssue():
1692 return None
1693
1694 try:
1695 props = self.GetIssueProperties()
1696 except urllib2.HTTPError:
1697 return 'error'
1698
1699 if props.get('closed'):
1700 # Issue is closed.
1701 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001702 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001703 # Issue is in the commit queue.
1704 return 'commit'
1705
1706 try:
1707 reviewers = self.GetApprovingReviewers()
1708 except urllib2.HTTPError:
1709 return 'error'
1710
1711 if reviewers:
1712 # Was LGTM'ed.
1713 return 'lgtm'
1714
1715 messages = props.get('messages') or []
1716
tandrii9d2c7a32016-06-22 03:42:45 -07001717 # Skip CQ messages that don't require owner's action.
1718 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1719 if 'Dry run:' in messages[-1]['text']:
1720 messages.pop()
1721 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1722 # This message always follows prior messages from CQ,
1723 # so skip this too.
1724 messages.pop()
1725 else:
1726 # This is probably a CQ messages warranting user attention.
1727 break
1728
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001729 if not messages:
1730 # No message was sent.
1731 return 'unsent'
1732 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07001733 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001734 return 'reply'
1735 return 'waiting'
1736
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001737 def UpdateDescriptionRemote(self, description):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001738 return self.RpcServer().update_description(
1739 self.GetIssue(), self.description)
1740
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001741 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001742 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001743
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001744 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07001745 return self.SetFlags({flag: value})
1746
1747 def SetFlags(self, flags):
1748 """Sets flags on this CL/patchset in Rietveld.
1749
1750 The latest patchset in Rietveld must be the same as latest known locally.
1751 """
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001752 if not self.GetPatchset():
1753 DieWithError('The patchset needs to match. Send another patchset.')
1754 try:
tandrii4b233bd2016-07-06 03:50:29 -07001755 return self.RpcServer().set_flags(
1756 self.GetIssue(), self.GetPatchset(), flags)
vapierfd77ac72016-06-16 08:33:57 -07001757 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001758 if e.code == 404:
1759 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1760 if e.code == 403:
1761 DieWithError(
1762 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1763 'match?') % (self.GetIssue(), self.GetPatchset()))
1764 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001765
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001766 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001767 """Returns an upload.RpcServer() to access this review's rietveld instance.
1768 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001769 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001770 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001771 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001772 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001773 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001774
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001775 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001776 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001777 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001778
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001779 def PatchsetSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001780 """Return the git setting that stores this change's most recent patchset."""
1781 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1782
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001783 def GetCodereviewServerSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001784 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001785 branch = self.GetBranch()
1786 if branch:
1787 return 'branch.%s.rietveldserver' % branch
1788 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001789
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001790 def _PostUnsetIssueProperties(self):
1791 """Which branch-specific properties to erase when unsetting issue."""
1792 return ['rietveldserver']
1793
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001794 def GetRieveldObjForPresubmit(self):
1795 return self.RpcServer()
1796
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001797 def SetCQState(self, new_state):
1798 props = self.GetIssueProperties()
1799 if props.get('private'):
1800 DieWithError('Cannot set-commit on private issue')
1801
1802 if new_state == _CQState.COMMIT:
1803 self.SetFlag('commit', '1')
1804 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07001805 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001806 else:
tandrii4b233bd2016-07-06 03:50:29 -07001807 assert new_state == _CQState.DRY_RUN
1808 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001809
1810
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001811 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1812 directory):
1813 # TODO(maruel): Use apply_issue.py
1814
1815 # PatchIssue should never be called with a dirty tree. It is up to the
1816 # caller to check this, but just in case we assert here since the
1817 # consequences of the caller not checking this could be dire.
1818 assert(not git_common.is_dirty_git_tree('apply'))
1819 assert(parsed_issue_arg.valid)
1820 self._changelist.issue = parsed_issue_arg.issue
1821 if parsed_issue_arg.hostname:
1822 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1823
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001824 if (isinstance(parsed_issue_arg, _RietveldParsedIssueNumberArgument) and
1825 parsed_issue_arg.patch_url):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001826 assert parsed_issue_arg.patchset
1827 patchset = parsed_issue_arg.patchset
1828 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1829 else:
1830 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1831 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1832
1833 # Switch up to the top-level directory, if necessary, in preparation for
1834 # applying the patch.
1835 top = settings.GetRelativeRoot()
1836 if top:
1837 os.chdir(top)
1838
1839 # Git patches have a/ at the beginning of source paths. We strip that out
1840 # with a sed script rather than the -p flag to patch so we can feed either
1841 # Git or svn-style patches into the same apply command.
1842 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1843 try:
1844 patch_data = subprocess2.check_output(
1845 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1846 except subprocess2.CalledProcessError:
1847 DieWithError('Git patch mungling failed.')
1848 logging.info(patch_data)
1849
1850 # We use "git apply" to apply the patch instead of "patch" so that we can
1851 # pick up file adds.
1852 # The --index flag means: also insert into the index (so we catch adds).
1853 cmd = ['git', 'apply', '--index', '-p0']
1854 if directory:
1855 cmd.extend(('--directory', directory))
1856 if reject:
1857 cmd.append('--reject')
1858 elif IsGitVersionAtLeast('1.7.12'):
1859 cmd.append('--3way')
1860 try:
1861 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
1862 stdin=patch_data, stdout=subprocess2.VOID)
1863 except subprocess2.CalledProcessError:
vapiera7fbd5a2016-06-16 09:17:49 -07001864 print('Failed to apply the patch')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001865 return 1
1866
1867 # If we had an issue, commit the current state and register the issue.
1868 if not nocommit:
1869 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
1870 'patch from issue %(i)s at patchset '
1871 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
1872 % {'i': self.GetIssue(), 'p': patchset})])
1873 self.SetIssue(self.GetIssue())
1874 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07001875 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001876 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001877 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001878 return 0
1879
1880 @staticmethod
1881 def ParseIssueURL(parsed_url):
1882 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1883 return None
1884 # Typical url: https://domain/<issue_number>[/[other]]
1885 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
1886 if match:
1887 return _RietveldParsedIssueNumberArgument(
1888 issue=int(match.group(1)),
1889 hostname=parsed_url.netloc)
1890 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
1891 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
1892 if match:
1893 return _RietveldParsedIssueNumberArgument(
1894 issue=int(match.group(1)),
1895 patchset=int(match.group(2)),
1896 hostname=parsed_url.netloc,
1897 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
1898 return None
1899
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001900 def CMDUploadChange(self, options, args, change):
1901 """Upload the patch to Rietveld."""
1902 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1903 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001904 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
1905 if options.emulate_svn_auto_props:
1906 upload_args.append('--emulate_svn_auto_props')
1907
1908 change_desc = None
1909
1910 if options.email is not None:
1911 upload_args.extend(['--email', options.email])
1912
1913 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07001914 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001915 upload_args.extend(['--title', options.title])
1916 if options.message:
1917 upload_args.extend(['--message', options.message])
1918 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07001919 print('This branch is associated with issue %s. '
1920 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001921 else:
nodirca166002016-06-27 10:59:51 -07001922 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001923 upload_args.extend(['--title', options.title])
1924 message = (options.title or options.message or
1925 CreateDescriptionFromLog(args))
1926 change_desc = ChangeDescription(message)
1927 if options.reviewers or options.tbr_owners:
1928 change_desc.update_reviewers(options.reviewers,
1929 options.tbr_owners,
1930 change)
1931 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07001932 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001933
1934 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07001935 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001936 return 1
1937
1938 upload_args.extend(['--message', change_desc.description])
1939 if change_desc.get_reviewers():
1940 upload_args.append('--reviewers=%s' % ','.join(
1941 change_desc.get_reviewers()))
1942 if options.send_mail:
1943 if not change_desc.get_reviewers():
1944 DieWithError("Must specify reviewers to send email.")
1945 upload_args.append('--send_mail')
1946
1947 # We check this before applying rietveld.private assuming that in
1948 # rietveld.cc only addresses which we can send private CLs to are listed
1949 # if rietveld.private is set, and so we should ignore rietveld.cc only
1950 # when --private is specified explicitly on the command line.
1951 if options.private:
1952 logging.warn('rietveld.cc is ignored since private flag is specified. '
1953 'You need to review and add them manually if necessary.')
1954 cc = self.GetCCListWithoutDefault()
1955 else:
1956 cc = self.GetCCList()
1957 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
1958 if cc:
1959 upload_args.extend(['--cc', cc])
1960
1961 if options.private or settings.GetDefaultPrivateFlag() == "True":
1962 upload_args.append('--private')
1963
1964 upload_args.extend(['--git_similarity', str(options.similarity)])
1965 if not options.find_copies:
1966 upload_args.extend(['--git_no_find_copies'])
1967
1968 # Include the upstream repo's URL in the change -- this is useful for
1969 # projects that have their source spread across multiple repos.
1970 remote_url = self.GetGitBaseUrlFromConfig()
1971 if not remote_url:
1972 if settings.GetIsGitSvn():
1973 remote_url = self.GetGitSvnRemoteUrl()
1974 else:
1975 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
1976 remote_url = '%s@%s' % (self.GetRemoteUrl(),
1977 self.GetUpstreamBranch().split('/')[-1])
1978 if remote_url:
1979 upload_args.extend(['--base_url', remote_url])
1980 remote, remote_branch = self.GetRemoteBranch()
1981 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
1982 settings.GetPendingRefPrefix())
1983 if target_ref:
1984 upload_args.extend(['--target_ref', target_ref])
1985
1986 # Look for dependent patchsets. See crbug.com/480453 for more details.
1987 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
1988 upstream_branch = ShortBranchName(upstream_branch)
1989 if remote is '.':
1990 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001991 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001992 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07001993 print()
1994 print('Skipping dependency patchset upload because git config '
1995 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
1996 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001997 else:
1998 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001999 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002000 auth_config=auth_config)
2001 branch_cl_issue_url = branch_cl.GetIssueURL()
2002 branch_cl_issue = branch_cl.GetIssue()
2003 branch_cl_patchset = branch_cl.GetPatchset()
2004 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2005 upload_args.extend(
2006 ['--depends_on_patchset', '%s:%s' % (
2007 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002008 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002009 '\n'
2010 'The current branch (%s) is tracking a local branch (%s) with '
2011 'an associated CL.\n'
2012 'Adding %s/#ps%s as a dependency patchset.\n'
2013 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2014 branch_cl_patchset))
2015
2016 project = settings.GetProject()
2017 if project:
2018 upload_args.extend(['--project', project])
2019
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002020 try:
2021 upload_args = ['upload'] + upload_args + args
2022 logging.info('upload.RealMain(%s)', upload_args)
2023 issue, patchset = upload.RealMain(upload_args)
2024 issue = int(issue)
2025 patchset = int(patchset)
2026 except KeyboardInterrupt:
2027 sys.exit(1)
2028 except:
2029 # If we got an exception after the user typed a description for their
2030 # change, back up the description before re-raising.
2031 if change_desc:
2032 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2033 print('\nGot exception while uploading -- saving description to %s\n' %
2034 backup_path)
2035 backup_file = open(backup_path, 'w')
2036 backup_file.write(change_desc.description)
2037 backup_file.close()
2038 raise
2039
2040 if not self.GetIssue():
2041 self.SetIssue(issue)
2042 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002043 return 0
2044
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002045
2046class _GerritChangelistImpl(_ChangelistCodereviewBase):
2047 def __init__(self, changelist, auth_config=None):
2048 # auth_config is Rietveld thing, kept here to preserve interface only.
2049 super(_GerritChangelistImpl, self).__init__(changelist)
2050 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002051 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002052 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002053 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002054
2055 def _GetGerritHost(self):
2056 # Lazy load of configs.
2057 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002058 if self._gerrit_host and '.' not in self._gerrit_host:
2059 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2060 # This happens for internal stuff http://crbug.com/614312.
2061 parsed = urlparse.urlparse(self.GetRemoteUrl())
2062 if parsed.scheme == 'sso':
2063 print('WARNING: using non https URLs for remote is likely broken\n'
2064 ' Your current remote is: %s' % self.GetRemoteUrl())
2065 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2066 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002067 return self._gerrit_host
2068
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002069 def _GetGitHost(self):
2070 """Returns git host to be used when uploading change to Gerrit."""
2071 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2072
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002073 def GetCodereviewServer(self):
2074 if not self._gerrit_server:
2075 # If we're on a branch then get the server potentially associated
2076 # with that branch.
2077 if self.GetIssue():
2078 gerrit_server_setting = self.GetCodereviewServerSetting()
2079 if gerrit_server_setting:
2080 self._gerrit_server = RunGit(['config', gerrit_server_setting],
2081 error_ok=True).strip()
2082 if self._gerrit_server:
2083 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
2084 if not self._gerrit_server:
2085 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2086 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002087 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002088 parts[0] = parts[0] + '-review'
2089 self._gerrit_host = '.'.join(parts)
2090 self._gerrit_server = 'https://%s' % self._gerrit_host
2091 return self._gerrit_server
2092
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002093 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00002094 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002095 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002096
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002097 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002098 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002099 if settings.GetGerritSkipEnsureAuthenticated():
2100 # For projects with unusual authentication schemes.
2101 # See http://crbug.com/603378.
2102 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002103 # Lazy-loader to identify Gerrit and Git hosts.
2104 if gerrit_util.GceAuthenticator.is_gce():
2105 return
2106 self.GetCodereviewServer()
2107 git_host = self._GetGitHost()
2108 assert self._gerrit_server and self._gerrit_host
2109 cookie_auth = gerrit_util.CookiesAuthenticator()
2110
2111 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2112 git_auth = cookie_auth.get_auth_header(git_host)
2113 if gerrit_auth and git_auth:
2114 if gerrit_auth == git_auth:
2115 return
2116 print((
2117 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2118 ' Check your %s or %s file for credentials of hosts:\n'
2119 ' %s\n'
2120 ' %s\n'
2121 ' %s') %
2122 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2123 git_host, self._gerrit_host,
2124 cookie_auth.get_new_password_message(git_host)))
2125 if not force:
2126 ask_for_data('If you know what you are doing, press Enter to continue, '
2127 'Ctrl+C to abort.')
2128 return
2129 else:
2130 missing = (
2131 [] if gerrit_auth else [self._gerrit_host] +
2132 [] if git_auth else [git_host])
2133 DieWithError('Credentials for the following hosts are required:\n'
2134 ' %s\n'
2135 'These are read from %s (or legacy %s)\n'
2136 '%s' % (
2137 '\n '.join(missing),
2138 cookie_auth.get_gitcookies_path(),
2139 cookie_auth.get_netrc_path(),
2140 cookie_auth.get_new_password_message(git_host)))
2141
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002142
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002143 def PatchsetSetting(self):
2144 """Return the git setting that stores this change's most recent patchset."""
2145 return 'branch.%s.gerritpatchset' % self.GetBranch()
2146
2147 def GetCodereviewServerSetting(self):
2148 """Returns the git setting that stores this change's Gerrit server."""
2149 branch = self.GetBranch()
2150 if branch:
2151 return 'branch.%s.gerritserver' % branch
2152 return None
2153
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002154 def _PostUnsetIssueProperties(self):
2155 """Which branch-specific properties to erase when unsetting issue."""
2156 return [
2157 'gerritserver',
2158 'gerritsquashhash',
2159 ]
2160
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002161 def GetRieveldObjForPresubmit(self):
2162 class ThisIsNotRietveldIssue(object):
2163 def __nonzero__(self):
2164 # This is a hack to make presubmit_support think that rietveld is not
2165 # defined, yet still ensure that calls directly result in a decent
2166 # exception message below.
2167 return False
2168
2169 def __getattr__(self, attr):
2170 print(
2171 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2172 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2173 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2174 'or use Rietveld for codereview.\n'
2175 'See also http://crbug.com/579160.' % attr)
2176 raise NotImplementedError()
2177 return ThisIsNotRietveldIssue()
2178
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002179 def GetGerritObjForPresubmit(self):
2180 return presubmit_support.GerritAccessor(self._GetGerritHost())
2181
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002182 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002183 """Apply a rough heuristic to give a simple summary of an issue's review
2184 or CQ status, assuming adherence to a common workflow.
2185
2186 Returns None if no issue for this branch, or one of the following keywords:
2187 * 'error' - error from review tool (including deleted issues)
2188 * 'unsent' - no reviewers added
2189 * 'waiting' - waiting for review
2190 * 'reply' - waiting for owner to reply to review
2191 * 'not lgtm' - Code-Review -2 from at least one approved reviewer
2192 * 'lgtm' - Code-Review +2 from at least one approved reviewer
2193 * 'commit' - in the commit queue
2194 * 'closed' - abandoned
2195 """
2196 if not self.GetIssue():
2197 return None
2198
2199 try:
2200 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
2201 except httplib.HTTPException:
2202 return 'error'
2203
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002204 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002205 return 'closed'
2206
2207 cq_label = data['labels'].get('Commit-Queue', {})
2208 if cq_label:
2209 # Vote value is a stringified integer, which we expect from 0 to 2.
2210 vote_value = cq_label.get('value', '0')
2211 vote_text = cq_label.get('values', {}).get(vote_value, '')
2212 if vote_text.lower() == 'commit':
2213 return 'commit'
2214
2215 lgtm_label = data['labels'].get('Code-Review', {})
2216 if lgtm_label:
2217 if 'rejected' in lgtm_label:
2218 return 'not lgtm'
2219 if 'approved' in lgtm_label:
2220 return 'lgtm'
2221
2222 if not data.get('reviewers', {}).get('REVIEWER', []):
2223 return 'unsent'
2224
2225 messages = data.get('messages', [])
2226 if messages:
2227 owner = data['owner'].get('_account_id')
2228 last_message_author = messages[-1].get('author', {}).get('_account_id')
2229 if owner != last_message_author:
2230 # Some reply from non-owner.
2231 return 'reply'
2232
2233 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002234
2235 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002236 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002237 return data['revisions'][data['current_revision']]['_number']
2238
2239 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002240 data = self._GetChangeDetail(['CURRENT_REVISION'])
2241 current_rev = data['current_revision']
2242 url = data['revisions'][current_rev]['fetch']['http']['url']
2243 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002244
2245 def UpdateDescriptionRemote(self, description):
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002246 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
2247 description)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002248
2249 def CloseIssue(self):
2250 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2251
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002252 def GetApprovingReviewers(self):
2253 """Returns a list of reviewers approving the change.
2254
2255 Note: not necessarily committers.
2256 """
2257 raise NotImplementedError()
2258
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002259 def SubmitIssue(self, wait_for_merge=True):
2260 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2261 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002262
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002263 def _GetChangeDetail(self, options=None, issue=None):
2264 options = options or []
2265 issue = issue or self.GetIssue()
2266 assert issue, 'issue required to query Gerrit'
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002267 return gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2268 options)
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002269
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002270 def CMDLand(self, force, bypass_hooks, verbose):
2271 if git_common.is_dirty_git_tree('land'):
2272 return 1
tandriid60367b2016-06-22 05:25:12 -07002273 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2274 if u'Commit-Queue' in detail.get('labels', {}):
2275 if not force:
2276 ask_for_data('\nIt seems this repository has a Commit Queue, '
2277 'which can test and land changes for you. '
2278 'Are you sure you wish to bypass it?\n'
2279 'Press Enter to continue, Ctrl+C to abort.')
2280
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002281 differs = True
2282 last_upload = RunGit(['config',
2283 'branch.%s.gerritsquashhash' % self.GetBranch()],
2284 error_ok=True).strip()
2285 # Note: git diff outputs nothing if there is no diff.
2286 if not last_upload or RunGit(['diff', last_upload]).strip():
2287 print('WARNING: some changes from local branch haven\'t been uploaded')
2288 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002289 if detail['current_revision'] == last_upload:
2290 differs = False
2291 else:
2292 print('WARNING: local branch contents differ from latest uploaded '
2293 'patchset')
2294 if differs:
2295 if not force:
2296 ask_for_data(
2297 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2298 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2299 elif not bypass_hooks:
2300 hook_results = self.RunHook(
2301 committing=True,
2302 may_prompt=not force,
2303 verbose=verbose,
2304 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2305 if not hook_results.should_continue():
2306 return 1
2307
2308 self.SubmitIssue(wait_for_merge=True)
2309 print('Issue %s has been submitted.' % self.GetIssueURL())
2310 return 0
2311
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002312 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2313 directory):
2314 assert not reject
2315 assert not nocommit
2316 assert not directory
2317 assert parsed_issue_arg.valid
2318
2319 self._changelist.issue = parsed_issue_arg.issue
2320
2321 if parsed_issue_arg.hostname:
2322 self._gerrit_host = parsed_issue_arg.hostname
2323 self._gerrit_server = 'https://%s' % self._gerrit_host
2324
2325 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2326
2327 if not parsed_issue_arg.patchset:
2328 # Use current revision by default.
2329 revision_info = detail['revisions'][detail['current_revision']]
2330 patchset = int(revision_info['_number'])
2331 else:
2332 patchset = parsed_issue_arg.patchset
2333 for revision_info in detail['revisions'].itervalues():
2334 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2335 break
2336 else:
2337 DieWithError('Couldn\'t find patchset %i in issue %i' %
2338 (parsed_issue_arg.patchset, self.GetIssue()))
2339
2340 fetch_info = revision_info['fetch']['http']
2341 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2342 RunGit(['cherry-pick', 'FETCH_HEAD'])
2343 self.SetIssue(self.GetIssue())
2344 self.SetPatchset(patchset)
2345 print('Committed patch for issue %i pathset %i locally' %
2346 (self.GetIssue(), self.GetPatchset()))
2347 return 0
2348
2349 @staticmethod
2350 def ParseIssueURL(parsed_url):
2351 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2352 return None
2353 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2354 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2355 # Short urls like https://domain/<issue_number> can be used, but don't allow
2356 # specifying the patchset (you'd 404), but we allow that here.
2357 if parsed_url.path == '/':
2358 part = parsed_url.fragment
2359 else:
2360 part = parsed_url.path
2361 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2362 if match:
2363 return _ParsedIssueNumberArgument(
2364 issue=int(match.group(2)),
2365 patchset=int(match.group(4)) if match.group(4) else None,
2366 hostname=parsed_url.netloc)
2367 return None
2368
tandrii16e0b4e2016-06-07 10:34:28 -07002369 def _GerritCommitMsgHookCheck(self, offer_removal):
2370 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2371 if not os.path.exists(hook):
2372 return
2373 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2374 # custom developer made one.
2375 data = gclient_utils.FileRead(hook)
2376 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2377 return
2378 print('Warning: you have Gerrit commit-msg hook installed.\n'
2379 'It is not neccessary for uploading with git cl in squash mode, '
2380 'and may interfere with it in subtle ways.\n'
2381 'We recommend you remove the commit-msg hook.')
2382 if offer_removal:
2383 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2384 if reply.lower().startswith('y'):
2385 gclient_utils.rm_file_or_tree(hook)
2386 print('Gerrit commit-msg hook removed.')
2387 else:
2388 print('OK, will keep Gerrit commit-msg hook in place.')
2389
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002390 def CMDUploadChange(self, options, args, change):
2391 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002392 if options.squash and options.no_squash:
2393 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002394
2395 if not options.squash and not options.no_squash:
2396 # Load default for user, repo, squash=true, in this order.
2397 options.squash = settings.GetSquashGerritUploads()
2398 elif options.no_squash:
2399 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002400
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002401 # We assume the remote called "origin" is the one we want.
2402 # It is probably not worthwhile to support different workflows.
2403 gerrit_remote = 'origin'
2404
2405 remote, remote_branch = self.GetRemoteBranch()
2406 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2407 pending_prefix='')
2408
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002409 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002410 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002411 if self.GetIssue():
2412 # Try to get the message from a previous upload.
2413 message = self.GetDescription()
2414 if not message:
2415 DieWithError(
2416 'failed to fetch description from current Gerrit issue %d\n'
2417 '%s' % (self.GetIssue(), self.GetIssueURL()))
2418 change_id = self._GetChangeDetail()['change_id']
2419 while True:
2420 footer_change_ids = git_footers.get_footer_change_id(message)
2421 if footer_change_ids == [change_id]:
2422 break
2423 if not footer_change_ids:
2424 message = git_footers.add_footer_change_id(message, change_id)
2425 print('WARNING: appended missing Change-Id to issue description')
2426 continue
2427 # There is already a valid footer but with different or several ids.
2428 # Doing this automatically is non-trivial as we don't want to lose
2429 # existing other footers, yet we want to append just 1 desired
2430 # Change-Id. Thus, just create a new footer, but let user verify the
2431 # new description.
2432 message = '%s\n\nChange-Id: %s' % (message, change_id)
2433 print(
2434 'WARNING: issue %s has Change-Id footer(s):\n'
2435 ' %s\n'
2436 'but issue has Change-Id %s, according to Gerrit.\n'
2437 'Please, check the proposed correction to the description, '
2438 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2439 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2440 change_id))
2441 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2442 if not options.force:
2443 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002444 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002445 message = change_desc.description
2446 if not message:
2447 DieWithError("Description is empty. Aborting...")
2448 # Continue the while loop.
2449 # Sanity check of this code - we should end up with proper message
2450 # footer.
2451 assert [change_id] == git_footers.get_footer_change_id(message)
2452 change_desc = ChangeDescription(message)
2453 else:
2454 change_desc = ChangeDescription(
2455 options.message or CreateDescriptionFromLog(args))
2456 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002457 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002458 if not change_desc.description:
2459 DieWithError("Description is empty. Aborting...")
2460 message = change_desc.description
2461 change_ids = git_footers.get_footer_change_id(message)
2462 if len(change_ids) > 1:
2463 DieWithError('too many Change-Id footers, at most 1 allowed.')
2464 if not change_ids:
2465 # Generate the Change-Id automatically.
2466 message = git_footers.add_footer_change_id(
2467 message, GenerateGerritChangeId(message))
2468 change_desc.set_description(message)
2469 change_ids = git_footers.get_footer_change_id(message)
2470 assert len(change_ids) == 1
2471 change_id = change_ids[0]
2472
2473 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2474 if remote is '.':
2475 # If our upstream branch is local, we base our squashed commit on its
2476 # squashed version.
2477 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2478 # Check the squashed hash of the parent.
2479 parent = RunGit(['config',
2480 'branch.%s.gerritsquashhash' % upstream_branch_name],
2481 error_ok=True).strip()
2482 # Verify that the upstream branch has been uploaded too, otherwise
2483 # Gerrit will create additional CLs when uploading.
2484 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2485 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002486 DieWithError(
2487 'Upload upstream branch %s first.\n'
tandrii2bdadf12016-07-12 12:27:54 -07002488 'Note: maybe you\'ve uploaded it with --no-squash. '
2489 'If so, then re-upload it with:\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002490 ' git cl upload --squash\n' % upstream_branch_name)
2491 else:
2492 parent = self.GetCommonAncestorWithUpstream()
2493
2494 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2495 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2496 '-m', message]).strip()
2497 else:
2498 change_desc = ChangeDescription(
2499 options.message or CreateDescriptionFromLog(args))
2500 if not change_desc.description:
2501 DieWithError("Description is empty. Aborting...")
2502
2503 if not git_footers.get_footer_change_id(change_desc.description):
2504 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002505 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2506 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002507 ref_to_push = 'HEAD'
2508 parent = '%s/%s' % (gerrit_remote, branch)
2509 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2510
2511 assert change_desc
2512 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2513 ref_to_push)]).splitlines()
2514 if len(commits) > 1:
2515 print('WARNING: This will upload %d commits. Run the following command '
2516 'to see which commits will be uploaded: ' % len(commits))
2517 print('git log %s..%s' % (parent, ref_to_push))
2518 print('You can also use `git squash-branch` to squash these into a '
2519 'single commit.')
2520 ask_for_data('About to upload; enter to confirm.')
2521
2522 if options.reviewers or options.tbr_owners:
2523 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2524 change)
2525
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002526 # Extra options that can be specified at push time. Doc:
2527 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2528 refspec_opts = []
2529 if options.title:
2530 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2531 # reverse on its side.
2532 if '_' in options.title:
2533 print('WARNING: underscores in title will be converted to spaces.')
2534 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2535
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002536 if options.send_mail:
2537 if not change_desc.get_reviewers():
2538 DieWithError('Must specify reviewers to send email.')
2539 refspec_opts.append('notify=ALL')
2540 else:
2541 refspec_opts.append('notify=NONE')
2542
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002543 cc = self.GetCCList().split(',')
2544 if options.cc:
2545 cc.extend(options.cc)
2546 cc = filter(None, cc)
2547 if cc:
tandrii@chromium.org074c2af2016-06-03 23:18:40 +00002548 refspec_opts.extend('cc=' + email.strip() for email in cc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002549
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002550 if change_desc.get_reviewers():
2551 refspec_opts.extend('r=' + email.strip()
2552 for email in change_desc.get_reviewers())
2553
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002554 refspec_suffix = ''
2555 if refspec_opts:
2556 refspec_suffix = '%' + ','.join(refspec_opts)
2557 assert ' ' not in refspec_suffix, (
2558 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002559 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002560
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002561 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002562 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002563 print_stdout=True,
2564 # Flush after every line: useful for seeing progress when running as
2565 # recipe.
2566 filter_fn=lambda _: sys.stdout.flush())
2567
2568 if options.squash:
2569 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2570 change_numbers = [m.group(1)
2571 for m in map(regex.match, push_stdout.splitlines())
2572 if m]
2573 if len(change_numbers) != 1:
2574 DieWithError(
2575 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2576 'Change-Id: %s') % (len(change_numbers), change_id))
2577 self.SetIssue(change_numbers[0])
2578 RunGit(['config', 'branch.%s.gerritsquashhash' % self.GetBranch(),
2579 ref_to_push])
2580 return 0
2581
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002582 def _AddChangeIdToCommitMessage(self, options, args):
2583 """Re-commits using the current message, assumes the commit hook is in
2584 place.
2585 """
2586 log_desc = options.message or CreateDescriptionFromLog(args)
2587 git_command = ['commit', '--amend', '-m', log_desc]
2588 RunGit(git_command)
2589 new_log_desc = CreateDescriptionFromLog(args)
2590 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002591 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002592 return new_log_desc
2593 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002594 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002595
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002596 def SetCQState(self, new_state):
2597 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002598 vote_map = {
2599 _CQState.NONE: 0,
2600 _CQState.DRY_RUN: 1,
2601 _CQState.COMMIT : 2,
2602 }
2603 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2604 labels={'Commit-Queue': vote_map[new_state]})
2605
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002606
2607_CODEREVIEW_IMPLEMENTATIONS = {
2608 'rietveld': _RietveldChangelistImpl,
2609 'gerrit': _GerritChangelistImpl,
2610}
2611
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002612
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002613def _add_codereview_select_options(parser):
2614 """Appends --gerrit and --rietveld options to force specific codereview."""
2615 parser.codereview_group = optparse.OptionGroup(
2616 parser, 'EXPERIMENTAL! Codereview override options')
2617 parser.add_option_group(parser.codereview_group)
2618 parser.codereview_group.add_option(
2619 '--gerrit', action='store_true',
2620 help='Force the use of Gerrit for codereview')
2621 parser.codereview_group.add_option(
2622 '--rietveld', action='store_true',
2623 help='Force the use of Rietveld for codereview')
2624
2625
2626def _process_codereview_select_options(parser, options):
2627 if options.gerrit and options.rietveld:
2628 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2629 options.forced_codereview = None
2630 if options.gerrit:
2631 options.forced_codereview = 'gerrit'
2632 elif options.rietveld:
2633 options.forced_codereview = 'rietveld'
2634
2635
tandriif9aefb72016-07-01 09:06:51 -07002636def _get_bug_line_values(default_project, bugs):
2637 """Given default_project and comma separated list of bugs, yields bug line
2638 values.
2639
2640 Each bug can be either:
2641 * a number, which is combined with default_project
2642 * string, which is left as is.
2643
2644 This function may produce more than one line, because bugdroid expects one
2645 project per line.
2646
2647 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2648 ['v8:123', 'chromium:789']
2649 """
2650 default_bugs = []
2651 others = []
2652 for bug in bugs.split(','):
2653 bug = bug.strip()
2654 if bug:
2655 try:
2656 default_bugs.append(int(bug))
2657 except ValueError:
2658 others.append(bug)
2659
2660 if default_bugs:
2661 default_bugs = ','.join(map(str, default_bugs))
2662 if default_project:
2663 yield '%s:%s' % (default_project, default_bugs)
2664 else:
2665 yield default_bugs
2666 for other in sorted(others):
2667 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2668 yield other
2669
2670
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002671class ChangeDescription(object):
2672 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002673 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002674 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002675
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002676 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002677 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002678
agable@chromium.org42c20792013-09-12 17:34:49 +00002679 @property # www.logilab.org/ticket/89786
2680 def description(self): # pylint: disable=E0202
2681 return '\n'.join(self._description_lines)
2682
2683 def set_description(self, desc):
2684 if isinstance(desc, basestring):
2685 lines = desc.splitlines()
2686 else:
2687 lines = [line.rstrip() for line in desc]
2688 while lines and not lines[0]:
2689 lines.pop(0)
2690 while lines and not lines[-1]:
2691 lines.pop(-1)
2692 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002693
piman@chromium.org336f9122014-09-04 02:16:55 +00002694 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002695 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002696 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002697 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002698 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002699 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002700
agable@chromium.org42c20792013-09-12 17:34:49 +00002701 # Get the set of R= and TBR= lines and remove them from the desciption.
2702 regexp = re.compile(self.R_LINE)
2703 matches = [regexp.match(line) for line in self._description_lines]
2704 new_desc = [l for i, l in enumerate(self._description_lines)
2705 if not matches[i]]
2706 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002707
agable@chromium.org42c20792013-09-12 17:34:49 +00002708 # Construct new unified R= and TBR= lines.
2709 r_names = []
2710 tbr_names = []
2711 for match in matches:
2712 if not match:
2713 continue
2714 people = cleanup_list([match.group(2).strip()])
2715 if match.group(1) == 'TBR':
2716 tbr_names.extend(people)
2717 else:
2718 r_names.extend(people)
2719 for name in r_names:
2720 if name not in reviewers:
2721 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002722 if add_owners_tbr:
2723 owners_db = owners.Database(change.RepositoryRoot(),
2724 fopen=file, os_path=os.path, glob=glob.glob)
2725 all_reviewers = set(tbr_names + reviewers)
2726 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2727 all_reviewers)
2728 tbr_names.extend(owners_db.reviewers_for(missing_files,
2729 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002730 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2731 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2732
2733 # Put the new lines in the description where the old first R= line was.
2734 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2735 if 0 <= line_loc < len(self._description_lines):
2736 if new_tbr_line:
2737 self._description_lines.insert(line_loc, new_tbr_line)
2738 if new_r_line:
2739 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002740 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002741 if new_r_line:
2742 self.append_footer(new_r_line)
2743 if new_tbr_line:
2744 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002745
tandriif9aefb72016-07-01 09:06:51 -07002746 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002747 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002748 self.set_description([
2749 '# Enter a description of the change.',
2750 '# This will be displayed on the codereview site.',
2751 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002752 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002753 '--------------------',
2754 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002755
agable@chromium.org42c20792013-09-12 17:34:49 +00002756 regexp = re.compile(self.BUG_LINE)
2757 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07002758 prefix = settings.GetBugPrefix()
2759 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
2760 for value in values:
2761 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
2762 self.append_footer('BUG=%s' % value)
2763
agable@chromium.org42c20792013-09-12 17:34:49 +00002764 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002765 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002766 if not content:
2767 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002768 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002769
2770 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002771 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2772 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002773 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002774 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002775
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002776 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00002777 """Adds a footer line to the description.
2778
2779 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
2780 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
2781 that Gerrit footers are always at the end.
2782 """
2783 parsed_footer_line = git_footers.parse_footer(line)
2784 if parsed_footer_line:
2785 # Line is a gerrit footer in the form: Footer-Key: any value.
2786 # Thus, must be appended observing Gerrit footer rules.
2787 self.set_description(
2788 git_footers.add_footer(self.description,
2789 key=parsed_footer_line[0],
2790 value=parsed_footer_line[1]))
2791 return
2792
2793 if not self._description_lines:
2794 self._description_lines.append(line)
2795 return
2796
2797 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
2798 if gerrit_footers:
2799 # git_footers.split_footers ensures that there is an empty line before
2800 # actual (gerrit) footers, if any. We have to keep it that way.
2801 assert top_lines and top_lines[-1] == ''
2802 top_lines, separator = top_lines[:-1], top_lines[-1:]
2803 else:
2804 separator = [] # No need for separator if there are no gerrit_footers.
2805
2806 prev_line = top_lines[-1] if top_lines else ''
2807 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
2808 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2809 top_lines.append('')
2810 top_lines.append(line)
2811 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002812
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002813 def get_reviewers(self):
2814 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002815 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
2816 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002817 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002818
2819
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002820def get_approving_reviewers(props):
2821 """Retrieves the reviewers that approved a CL from the issue properties with
2822 messages.
2823
2824 Note that the list may contain reviewers that are not committer, thus are not
2825 considered by the CQ.
2826 """
2827 return sorted(
2828 set(
2829 message['sender']
2830 for message in props['messages']
2831 if message['approval'] and message['sender'] in props['reviewers']
2832 )
2833 )
2834
2835
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002836def FindCodereviewSettingsFile(filename='codereview.settings'):
2837 """Finds the given file starting in the cwd and going up.
2838
2839 Only looks up to the top of the repository unless an
2840 'inherit-review-settings-ok' file exists in the root of the repository.
2841 """
2842 inherit_ok_file = 'inherit-review-settings-ok'
2843 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002844 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002845 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2846 root = '/'
2847 while True:
2848 if filename in os.listdir(cwd):
2849 if os.path.isfile(os.path.join(cwd, filename)):
2850 return open(os.path.join(cwd, filename))
2851 if cwd == root:
2852 break
2853 cwd = os.path.dirname(cwd)
2854
2855
2856def LoadCodereviewSettingsFromFile(fileobj):
2857 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002858 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002859
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002860 def SetProperty(name, setting, unset_error_ok=False):
2861 fullname = 'rietveld.' + name
2862 if setting in keyvals:
2863 RunGit(['config', fullname, keyvals[setting]])
2864 else:
2865 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2866
2867 SetProperty('server', 'CODE_REVIEW_SERVER')
2868 # Only server setting is required. Other settings can be absent.
2869 # In that case, we ignore errors raised during option deletion attempt.
2870 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002871 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002872 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2873 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002874 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002875 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002876 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2877 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002878 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002879 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002880 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002881 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2882 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002883
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002884 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002885 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002886
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002887 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07002888 RunGit(['config', 'gerrit.squash-uploads',
2889 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002890
tandrii@chromium.org28253532016-04-14 13:46:56 +00002891 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00002892 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00002893 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
2894
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002895 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2896 #should be of the form
2897 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2898 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2899 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2900 keyvals['ORIGIN_URL_CONFIG']])
2901
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002902
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002903def urlretrieve(source, destination):
2904 """urllib is broken for SSL connections via a proxy therefore we
2905 can't use urllib.urlretrieve()."""
2906 with open(destination, 'w') as f:
2907 f.write(urllib2.urlopen(source).read())
2908
2909
ukai@chromium.org712d6102013-11-27 00:52:58 +00002910def hasSheBang(fname):
2911 """Checks fname is a #! script."""
2912 with open(fname) as f:
2913 return f.read(2).startswith('#!')
2914
2915
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00002916# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
2917def DownloadHooks(*args, **kwargs):
2918 pass
2919
2920
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002921def DownloadGerritHook(force):
2922 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002923
2924 Args:
2925 force: True to update hooks. False to install hooks if not present.
2926 """
2927 if not settings.GetIsGerrit():
2928 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00002929 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002930 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2931 if not os.access(dst, os.X_OK):
2932 if os.path.exists(dst):
2933 if not force:
2934 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002935 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002936 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002937 if not hasSheBang(dst):
2938 DieWithError('Not a script: %s\n'
2939 'You need to download from\n%s\n'
2940 'into .git/hooks/commit-msg and '
2941 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002942 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2943 except Exception:
2944 if os.path.exists(dst):
2945 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002946 DieWithError('\nFailed to download hooks.\n'
2947 'You need to download from\n%s\n'
2948 'into .git/hooks/commit-msg and '
2949 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002950
2951
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002952
2953def GetRietveldCodereviewSettingsInteractively():
2954 """Prompt the user for settings."""
2955 server = settings.GetDefaultServerUrl(error_ok=True)
2956 prompt = 'Rietveld server (host[:port])'
2957 prompt += ' [%s]' % (server or DEFAULT_SERVER)
2958 newserver = ask_for_data(prompt + ':')
2959 if not server and not newserver:
2960 newserver = DEFAULT_SERVER
2961 if newserver:
2962 newserver = gclient_utils.UpgradeToHttps(newserver)
2963 if newserver != server:
2964 RunGit(['config', 'rietveld.server', newserver])
2965
2966 def SetProperty(initial, caption, name, is_url):
2967 prompt = caption
2968 if initial:
2969 prompt += ' ("x" to clear) [%s]' % initial
2970 new_val = ask_for_data(prompt + ':')
2971 if new_val == 'x':
2972 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
2973 elif new_val:
2974 if is_url:
2975 new_val = gclient_utils.UpgradeToHttps(new_val)
2976 if new_val != initial:
2977 RunGit(['config', 'rietveld.' + name, new_val])
2978
2979 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
2980 SetProperty(settings.GetDefaultPrivateFlag(),
2981 'Private flag (rietveld only)', 'private', False)
2982 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
2983 'tree-status-url', False)
2984 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
2985 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
2986 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
2987 'run-post-upload-hook', False)
2988
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002989@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002990def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002991 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002992
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002993 print('WARNING: git cl config works for Rietveld only.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002994 'For Gerrit, see http://crbug.com/603116.')
2995 # TODO(tandrii): add Gerrit support as part of http://crbug.com/603116.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00002996 parser.add_option('--activate-update', action='store_true',
2997 help='activate auto-updating [rietveld] section in '
2998 '.git/config')
2999 parser.add_option('--deactivate-update', action='store_true',
3000 help='deactivate auto-updating [rietveld] section in '
3001 '.git/config')
3002 options, args = parser.parse_args(args)
3003
3004 if options.deactivate_update:
3005 RunGit(['config', 'rietveld.autoupdate', 'false'])
3006 return
3007
3008 if options.activate_update:
3009 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3010 return
3011
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003012 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003013 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003014 return 0
3015
3016 url = args[0]
3017 if not url.endswith('codereview.settings'):
3018 url = os.path.join(url, 'codereview.settings')
3019
3020 # Load code review settings and download hooks (if available).
3021 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3022 return 0
3023
3024
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003025def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003026 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003027 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3028 branch = ShortBranchName(branchref)
3029 _, args = parser.parse_args(args)
3030 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003031 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003032 return RunGit(['config', 'branch.%s.base-url' % branch],
3033 error_ok=False).strip()
3034 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003035 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003036 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3037 error_ok=False).strip()
3038
3039
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003040def color_for_status(status):
3041 """Maps a Changelist status to color, for CMDstatus and other tools."""
3042 return {
3043 'unsent': Fore.RED,
3044 'waiting': Fore.BLUE,
3045 'reply': Fore.YELLOW,
3046 'lgtm': Fore.GREEN,
3047 'commit': Fore.MAGENTA,
3048 'closed': Fore.CYAN,
3049 'error': Fore.WHITE,
3050 }.get(status, Fore.WHITE)
3051
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003052
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003053def get_cl_statuses(changes, fine_grained, max_processes=None):
3054 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003055
3056 If fine_grained is true, this will fetch CL statuses from the server.
3057 Otherwise, simply indicate if there's a matching url for the given branches.
3058
3059 If max_processes is specified, it is used as the maximum number of processes
3060 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3061 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003062
3063 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003064 """
3065 # Silence upload.py otherwise it becomes unwieldly.
3066 upload.verbosity = 0
3067
3068 if fine_grained:
3069 # Process one branch synchronously to work through authentication, then
3070 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003071 if changes:
3072 fetch = lambda cl: (cl, cl.GetStatus())
3073 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003074
kmarshall3bff56b2016-06-06 18:31:47 -07003075 if not changes:
3076 # Exit early if there was only one branch to fetch.
3077 return
3078
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003079 changes_to_fetch = changes[1:]
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003080 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003081 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003082 if max_processes is not None
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003083 else len(changes_to_fetch))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003084
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003085 fetched_cls = set()
3086 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003087 while True:
3088 try:
3089 row = it.next(timeout=5)
3090 except multiprocessing.TimeoutError:
3091 break
3092
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003093 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003094 yield row
3095
3096 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003097 for cl in set(changes_to_fetch) - fetched_cls:
3098 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003099
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003100 else:
3101 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003102 for cl in changes:
3103 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003104
rmistry@google.com2dd99862015-06-22 12:22:18 +00003105
3106def upload_branch_deps(cl, args):
3107 """Uploads CLs of local branches that are dependents of the current branch.
3108
3109 If the local branch dependency tree looks like:
3110 test1 -> test2.1 -> test3.1
3111 -> test3.2
3112 -> test2.2 -> test3.3
3113
3114 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3115 run on the dependent branches in this order:
3116 test2.1, test3.1, test3.2, test2.2, test3.3
3117
3118 Note: This function does not rebase your local dependent branches. Use it when
3119 you make a change to the parent branch that will not conflict with its
3120 dependent branches, and you would like their dependencies updated in
3121 Rietveld.
3122 """
3123 if git_common.is_dirty_git_tree('upload-branch-deps'):
3124 return 1
3125
3126 root_branch = cl.GetBranch()
3127 if root_branch is None:
3128 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3129 'Get on a branch!')
3130 if not cl.GetIssue() or not cl.GetPatchset():
3131 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3132 'patchset dependencies without an uploaded CL.')
3133
3134 branches = RunGit(['for-each-ref',
3135 '--format=%(refname:short) %(upstream:short)',
3136 'refs/heads'])
3137 if not branches:
3138 print('No local branches found.')
3139 return 0
3140
3141 # Create a dictionary of all local branches to the branches that are dependent
3142 # on it.
3143 tracked_to_dependents = collections.defaultdict(list)
3144 for b in branches.splitlines():
3145 tokens = b.split()
3146 if len(tokens) == 2:
3147 branch_name, tracked = tokens
3148 tracked_to_dependents[tracked].append(branch_name)
3149
vapiera7fbd5a2016-06-16 09:17:49 -07003150 print()
3151 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003152 dependents = []
3153 def traverse_dependents_preorder(branch, padding=''):
3154 dependents_to_process = tracked_to_dependents.get(branch, [])
3155 padding += ' '
3156 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003157 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003158 dependents.append(dependent)
3159 traverse_dependents_preorder(dependent, padding)
3160 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003161 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003162
3163 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003164 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003165 return 0
3166
vapiera7fbd5a2016-06-16 09:17:49 -07003167 print('This command will checkout all dependent branches and run '
3168 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003169 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3170
andybons@chromium.org962f9462016-02-03 20:00:42 +00003171 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003172 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003173 args.extend(['-t', 'Updated patchset dependency'])
3174
rmistry@google.com2dd99862015-06-22 12:22:18 +00003175 # Record all dependents that failed to upload.
3176 failures = {}
3177 # Go through all dependents, checkout the branch and upload.
3178 try:
3179 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003180 print()
3181 print('--------------------------------------')
3182 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003183 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003184 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003185 try:
3186 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003187 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003188 failures[dependent_branch] = 1
3189 except: # pylint: disable=W0702
3190 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003191 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003192 finally:
3193 # Swap back to the original root branch.
3194 RunGit(['checkout', '-q', root_branch])
3195
vapiera7fbd5a2016-06-16 09:17:49 -07003196 print()
3197 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003198 for dependent_branch in dependents:
3199 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003200 print(' %s : %s' % (dependent_branch, upload_status))
3201 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003202
3203 return 0
3204
3205
kmarshall3bff56b2016-06-06 18:31:47 -07003206def CMDarchive(parser, args):
3207 """Archives and deletes branches associated with closed changelists."""
3208 parser.add_option(
3209 '-j', '--maxjobs', action='store', type=int,
3210 help='The maximum number of jobs to use when retrieving review status')
3211 parser.add_option(
3212 '-f', '--force', action='store_true',
3213 help='Bypasses the confirmation prompt.')
3214
3215 auth.add_auth_options(parser)
3216 options, args = parser.parse_args(args)
3217 if args:
3218 parser.error('Unsupported args: %s' % ' '.join(args))
3219 auth_config = auth.extract_auth_config_from_options(options)
3220
3221 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3222 if not branches:
3223 return 0
3224
vapiera7fbd5a2016-06-16 09:17:49 -07003225 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003226 changes = [Changelist(branchref=b, auth_config=auth_config)
3227 for b in branches.splitlines()]
3228 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3229 statuses = get_cl_statuses(changes,
3230 fine_grained=True,
3231 max_processes=options.maxjobs)
3232 proposal = [(cl.GetBranch(),
3233 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3234 for cl, status in statuses
3235 if status == 'closed']
3236 proposal.sort()
3237
3238 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003239 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003240 return 0
3241
3242 current_branch = GetCurrentBranch()
3243
vapiera7fbd5a2016-06-16 09:17:49 -07003244 print('\nBranches with closed issues that will be archived:\n')
3245 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
kmarshall3bff56b2016-06-06 18:31:47 -07003246 for next_item in proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003247 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003248
3249 if any(branch == current_branch for branch, _ in proposal):
3250 print('You are currently on a branch \'%s\' which is associated with a '
3251 'closed codereview issue, so archive cannot proceed. Please '
3252 'checkout another branch and run this command again.' %
3253 current_branch)
3254 return 1
3255
3256 if not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003257 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3258 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003259 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003260 return 1
3261
3262 for branch, tagname in proposal:
3263 RunGit(['tag', tagname, branch])
3264 RunGit(['branch', '-D', branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003265 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003266
3267 return 0
3268
3269
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003270def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003271 """Show status of changelists.
3272
3273 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003274 - Red not sent for review or broken
3275 - Blue waiting for review
3276 - Yellow waiting for you to reply to review
3277 - Green LGTM'ed
3278 - Magenta in the commit queue
3279 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003280
3281 Also see 'git cl comments'.
3282 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003283 parser.add_option('--field',
3284 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003285 parser.add_option('-f', '--fast', action='store_true',
3286 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003287 parser.add_option(
3288 '-j', '--maxjobs', action='store', type=int,
3289 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003290
3291 auth.add_auth_options(parser)
3292 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003293 if args:
3294 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003295 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003296
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003297 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003298 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003299 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003300 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003301 elif options.field == 'id':
3302 issueid = cl.GetIssue()
3303 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003304 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003305 elif options.field == 'patch':
3306 patchset = cl.GetPatchset()
3307 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003308 print(patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003309 elif options.field == 'url':
3310 url = cl.GetIssueURL()
3311 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003312 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003313 return 0
3314
3315 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3316 if not branches:
3317 print('No local branch found.')
3318 return 0
3319
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003320 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003321 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003322 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003323 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003324 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003325 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003326 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003327
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003328 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003329 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3330 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3331 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003332 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003333 c, status = output.next()
3334 branch_statuses[c.GetBranch()] = status
3335 status = branch_statuses.pop(branch)
3336 url = cl.GetIssueURL()
3337 if url and (not status or status == 'error'):
3338 # The issue probably doesn't exist anymore.
3339 url += ' (broken)'
3340
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003341 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003342 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003343 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003344 color = ''
3345 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003346 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003347 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003348 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003349 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003350
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003351 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003352 print()
3353 print('Current branch:',)
3354 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003355 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003356 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003357 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003358 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003359 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003360 print('Issue description:')
3361 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003362 return 0
3363
3364
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003365def colorize_CMDstatus_doc():
3366 """To be called once in main() to add colors to git cl status help."""
3367 colors = [i for i in dir(Fore) if i[0].isupper()]
3368
3369 def colorize_line(line):
3370 for color in colors:
3371 if color in line.upper():
3372 # Extract whitespaces first and the leading '-'.
3373 indent = len(line) - len(line.lstrip(' ')) + 1
3374 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3375 return line
3376
3377 lines = CMDstatus.__doc__.splitlines()
3378 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3379
3380
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003381@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003382def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003383 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003384
3385 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003386 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003387 parser.add_option('-r', '--reverse', action='store_true',
3388 help='Lookup the branch(es) for the specified issues. If '
3389 'no issues are specified, all branches with mapped '
3390 'issues will be listed.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003391 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003392 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003393 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003394
dnj@chromium.org406c4402015-03-03 17:22:28 +00003395 if options.reverse:
3396 branches = RunGit(['for-each-ref', 'refs/heads',
3397 '--format=%(refname:short)']).splitlines()
3398
3399 # Reverse issue lookup.
3400 issue_branch_map = {}
3401 for branch in branches:
3402 cl = Changelist(branchref=branch)
3403 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3404 if not args:
3405 args = sorted(issue_branch_map.iterkeys())
3406 for issue in args:
3407 if not issue:
3408 continue
vapiera7fbd5a2016-06-16 09:17:49 -07003409 print('Branch for issue number %s: %s' % (
3410 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
dnj@chromium.org406c4402015-03-03 17:22:28 +00003411 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003412 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003413 if len(args) > 0:
3414 try:
3415 issue = int(args[0])
3416 except ValueError:
3417 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003418 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003419 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003420 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003421 return 0
3422
3423
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003424def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003425 """Shows or posts review comments for any changelist."""
3426 parser.add_option('-a', '--add-comment', dest='comment',
3427 help='comment to add to an issue')
3428 parser.add_option('-i', dest='issue',
3429 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003430 parser.add_option('-j', '--json-file',
3431 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003432 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003433 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003434 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003435
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003436 issue = None
3437 if options.issue:
3438 try:
3439 issue = int(options.issue)
3440 except ValueError:
3441 DieWithError('A review issue id is expected to be a number')
3442
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003443 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003444
3445 if options.comment:
3446 cl.AddComment(options.comment)
3447 return 0
3448
3449 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003450 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003451 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003452 summary.append({
3453 'date': message['date'],
3454 'lgtm': False,
3455 'message': message['text'],
3456 'not_lgtm': False,
3457 'sender': message['sender'],
3458 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003459 if message['disapproval']:
3460 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003461 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003462 elif message['approval']:
3463 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003464 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003465 elif message['sender'] == data['owner_email']:
3466 color = Fore.MAGENTA
3467 else:
3468 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003469 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003470 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003471 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003472 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003473 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003474 if options.json_file:
3475 with open(options.json_file, 'wb') as f:
3476 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003477 return 0
3478
3479
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003480@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003481def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003482 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003483 parser.add_option('-d', '--display', action='store_true',
3484 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003485 parser.add_option('-n', '--new-description',
3486 help='New description to set for this issue (- for stdin)')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003487
3488 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003489 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003490 options, args = parser.parse_args(args)
3491 _process_codereview_select_options(parser, options)
3492
3493 target_issue = None
3494 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07003495 target_issue = ParseIssueNumberArgument(args[0])
3496 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003497 parser.print_help()
3498 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003499
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003500 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003501
martiniss6eda05f2016-06-30 10:18:35 -07003502 kwargs = {
3503 'auth_config': auth_config,
3504 'codereview': options.forced_codereview,
3505 }
3506 if target_issue:
3507 kwargs['issue'] = target_issue.issue
3508 if options.forced_codereview == 'rietveld':
3509 kwargs['rietveld_server'] = target_issue.hostname
3510
3511 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003512
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003513 if not cl.GetIssue():
3514 DieWithError('This branch has no associated changelist.')
3515 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003516
smut@google.com34fb6b12015-07-13 20:03:26 +00003517 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003518 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003519 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003520
3521 if options.new_description:
3522 text = options.new_description
3523 if text == '-':
3524 text = '\n'.join(l.rstrip() for l in sys.stdin)
3525
3526 description.set_description(text)
3527 else:
3528 description.prompt()
3529
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003530 if cl.GetDescription() != description.description:
3531 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003532 return 0
3533
3534
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003535def CreateDescriptionFromLog(args):
3536 """Pulls out the commit log to use as a base for the CL description."""
3537 log_args = []
3538 if len(args) == 1 and not args[0].endswith('.'):
3539 log_args = [args[0] + '..']
3540 elif len(args) == 1 and args[0].endswith('...'):
3541 log_args = [args[0][:-1]]
3542 elif len(args) == 2:
3543 log_args = [args[0] + '..' + args[1]]
3544 else:
3545 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003546 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003547
3548
thestig@chromium.org44202a22014-03-11 19:22:18 +00003549def CMDlint(parser, args):
3550 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003551 parser.add_option('--filter', action='append', metavar='-x,+y',
3552 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003553 auth.add_auth_options(parser)
3554 options, args = parser.parse_args(args)
3555 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003556
3557 # Access to a protected member _XX of a client class
3558 # pylint: disable=W0212
3559 try:
3560 import cpplint
3561 import cpplint_chromium
3562 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003563 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003564 return 1
3565
3566 # Change the current working directory before calling lint so that it
3567 # shows the correct base.
3568 previous_cwd = os.getcwd()
3569 os.chdir(settings.GetRoot())
3570 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003571 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003572 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3573 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003574 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003575 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003576 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003577
3578 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003579 command = args + files
3580 if options.filter:
3581 command = ['--filter=' + ','.join(options.filter)] + command
3582 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003583
3584 white_regex = re.compile(settings.GetLintRegex())
3585 black_regex = re.compile(settings.GetLintIgnoreRegex())
3586 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3587 for filename in filenames:
3588 if white_regex.match(filename):
3589 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07003590 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003591 else:
3592 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3593 extra_check_functions)
3594 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003595 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003596 finally:
3597 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07003598 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003599 if cpplint._cpplint_state.error_count != 0:
3600 return 1
3601 return 0
3602
3603
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003604def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003605 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003606 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003607 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003608 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003609 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003610 auth.add_auth_options(parser)
3611 options, args = parser.parse_args(args)
3612 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003613
sbc@chromium.org71437c02015-04-09 19:29:40 +00003614 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07003615 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003616 return 1
3617
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003618 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003619 if args:
3620 base_branch = args[0]
3621 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003622 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003623 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003624
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003625 cl.RunHook(
3626 committing=not options.upload,
3627 may_prompt=False,
3628 verbose=options.verbose,
3629 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003630 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003631
3632
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003633def GenerateGerritChangeId(message):
3634 """Returns Ixxxxxx...xxx change id.
3635
3636 Works the same way as
3637 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3638 but can be called on demand on all platforms.
3639
3640 The basic idea is to generate git hash of a state of the tree, original commit
3641 message, author/committer info and timestamps.
3642 """
3643 lines = []
3644 tree_hash = RunGitSilent(['write-tree'])
3645 lines.append('tree %s' % tree_hash.strip())
3646 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3647 if code == 0:
3648 lines.append('parent %s' % parent.strip())
3649 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3650 lines.append('author %s' % author.strip())
3651 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3652 lines.append('committer %s' % committer.strip())
3653 lines.append('')
3654 # Note: Gerrit's commit-hook actually cleans message of some lines and
3655 # whitespace. This code is not doing this, but it clearly won't decrease
3656 # entropy.
3657 lines.append(message)
3658 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3659 stdin='\n'.join(lines))
3660 return 'I%s' % change_hash.strip()
3661
3662
wittman@chromium.org455dc922015-01-26 20:15:50 +00003663def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3664 """Computes the remote branch ref to use for the CL.
3665
3666 Args:
3667 remote (str): The git remote for the CL.
3668 remote_branch (str): The git remote branch for the CL.
3669 target_branch (str): The target branch specified by the user.
3670 pending_prefix (str): The pending prefix from the settings.
3671 """
3672 if not (remote and remote_branch):
3673 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003674
wittman@chromium.org455dc922015-01-26 20:15:50 +00003675 if target_branch:
3676 # Cannonicalize branch references to the equivalent local full symbolic
3677 # refs, which are then translated into the remote full symbolic refs
3678 # below.
3679 if '/' not in target_branch:
3680 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3681 else:
3682 prefix_replacements = (
3683 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3684 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3685 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3686 )
3687 match = None
3688 for regex, replacement in prefix_replacements:
3689 match = re.search(regex, target_branch)
3690 if match:
3691 remote_branch = target_branch.replace(match.group(0), replacement)
3692 break
3693 if not match:
3694 # This is a branch path but not one we recognize; use as-is.
3695 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003696 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3697 # Handle the refs that need to land in different refs.
3698 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003699
wittman@chromium.org455dc922015-01-26 20:15:50 +00003700 # Create the true path to the remote branch.
3701 # Does the following translation:
3702 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3703 # * refs/remotes/origin/master -> refs/heads/master
3704 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3705 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3706 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3707 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3708 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3709 'refs/heads/')
3710 elif remote_branch.startswith('refs/remotes/branch-heads'):
3711 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3712 # If a pending prefix exists then replace refs/ with it.
3713 if pending_prefix:
3714 remote_branch = remote_branch.replace('refs/', pending_prefix)
3715 return remote_branch
3716
3717
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003718def cleanup_list(l):
3719 """Fixes a list so that comma separated items are put as individual items.
3720
3721 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3722 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3723 """
3724 items = sum((i.split(',') for i in l), [])
3725 stripped_items = (i.strip() for i in items)
3726 return sorted(filter(None, stripped_items))
3727
3728
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003729@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003730def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003731 """Uploads the current changelist to codereview.
3732
3733 Can skip dependency patchset uploads for a branch by running:
3734 git config branch.branch_name.skip-deps-uploads True
3735 To unset run:
3736 git config --unset branch.branch_name.skip-deps-uploads
3737 Can also set the above globally by using the --global flag.
3738 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003739 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3740 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003741 parser.add_option('--bypass-watchlists', action='store_true',
3742 dest='bypass_watchlists',
3743 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003744 parser.add_option('-f', action='store_true', dest='force',
3745 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003746 parser.add_option('-m', dest='message', help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07003747 parser.add_option('-b', '--bug',
3748 help='pre-populate the bug number(s) for this issue. '
3749 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07003750 parser.add_option('--message-file', dest='message_file',
3751 help='file which contains message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003752 parser.add_option('-t', dest='title',
3753 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003754 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003755 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003756 help='reviewer email addresses')
3757 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003758 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003759 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003760 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003761 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003762 parser.add_option('--emulate_svn_auto_props',
3763 '--emulate-svn-auto-props',
3764 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003765 dest="emulate_svn_auto_props",
3766 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003767 parser.add_option('-c', '--use-commit-queue', action='store_true',
3768 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003769 parser.add_option('--private', action='store_true',
3770 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003771 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003772 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003773 metavar='TARGET',
3774 help='Apply CL to remote ref TARGET. ' +
3775 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003776 parser.add_option('--squash', action='store_true',
3777 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003778 parser.add_option('--no-squash', action='store_true',
3779 help='Don\'t squash multiple commits into one ' +
3780 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003781 parser.add_option('--email', default=None,
3782 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003783 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3784 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003785 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3786 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003787 help='Send the patchset to do a CQ dry run right after '
3788 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003789 parser.add_option('--dependencies', action='store_true',
3790 help='Uploads CLs of all the local branches that depend on '
3791 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003792
rmistry@google.com2dd99862015-06-22 12:22:18 +00003793 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003794 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003795 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003796 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003797 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003798 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003799 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003800
sbc@chromium.org71437c02015-04-09 19:29:40 +00003801 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003802 return 1
3803
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003804 options.reviewers = cleanup_list(options.reviewers)
3805 options.cc = cleanup_list(options.cc)
3806
tandriib80458a2016-06-23 12:20:07 -07003807 if options.message_file:
3808 if options.message:
3809 parser.error('only one of --message and --message-file allowed.')
3810 options.message = gclient_utils.FileRead(options.message_file)
3811 options.message_file = None
3812
tandrii4d0545a2016-07-06 03:56:49 -07003813 if options.cq_dry_run and options.use_commit_queue:
3814 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
3815
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003816 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3817 settings.GetIsGerrit()
3818
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003819 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00003820 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003821
3822
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003823def IsSubmoduleMergeCommit(ref):
3824 # When submodules are added to the repo, we expect there to be a single
3825 # non-git-svn merge commit at remote HEAD with a signature comment.
3826 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003827 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003828 return RunGit(cmd) != ''
3829
3830
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003831def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003832 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003833
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003834 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3835 upstream and closes the issue automatically and atomically.
3836
3837 Otherwise (in case of Rietveld):
3838 Squashes branch into a single commit.
3839 Updates changelog with metadata (e.g. pointer to review).
3840 Pushes/dcommits the code upstream.
3841 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003842 """
3843 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3844 help='bypass upload presubmit hook')
3845 parser.add_option('-m', dest='message',
3846 help="override review description")
3847 parser.add_option('-f', action='store_true', dest='force',
3848 help="force yes to questions (don't prompt)")
3849 parser.add_option('-c', dest='contributor',
3850 help="external contributor for patch (appended to " +
3851 "description and used as author for git). Should be " +
3852 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003853 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003854 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003855 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003856 auth_config = auth.extract_auth_config_from_options(options)
3857
3858 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003859
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003860 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3861 if cl.IsGerrit():
3862 if options.message:
3863 # This could be implemented, but it requires sending a new patch to
3864 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3865 # Besides, Gerrit has the ability to change the commit message on submit
3866 # automatically, thus there is no need to support this option (so far?).
3867 parser.error('-m MESSAGE option is not supported for Gerrit.')
3868 if options.contributor:
3869 parser.error(
3870 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3871 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3872 'the contributor\'s "name <email>". If you can\'t upload such a '
3873 'commit for review, contact your repository admin and request'
3874 '"Forge-Author" permission.')
3875 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3876 options.verbose)
3877
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003878 current = cl.GetBranch()
3879 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3880 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07003881 print()
3882 print('Attempting to push branch %r into another local branch!' % current)
3883 print()
3884 print('Either reparent this branch on top of origin/master:')
3885 print(' git reparent-branch --root')
3886 print()
3887 print('OR run `git rebase-update` if you think the parent branch is ')
3888 print('already committed.')
3889 print()
3890 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003891 return 1
3892
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003893 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003894 # Default to merging against our best guess of the upstream branch.
3895 args = [cl.GetUpstreamBranch()]
3896
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003897 if options.contributor:
3898 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07003899 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003900 return 1
3901
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003902 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003903 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003904
sbc@chromium.org71437c02015-04-09 19:29:40 +00003905 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003906 return 1
3907
3908 # This rev-list syntax means "show all commits not in my branch that
3909 # are in base_branch".
3910 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3911 base_branch]).splitlines()
3912 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07003913 print('Base branch "%s" has %d commits '
3914 'not in this branch.' % (base_branch, len(upstream_commits)))
3915 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003916 return 1
3917
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003918 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003919 svn_head = None
3920 if cmd == 'dcommit' or base_has_submodules:
3921 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
3922 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003923
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003924 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003925 # If the base_head is a submodule merge commit, the first parent of the
3926 # base_head should be a git-svn commit, which is what we're interested in.
3927 base_svn_head = base_branch
3928 if base_has_submodules:
3929 base_svn_head += '^1'
3930
3931 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003932 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07003933 print('This branch has %d additional commits not upstreamed yet.'
3934 % len(extra_commits.splitlines()))
3935 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
3936 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003937 return 1
3938
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003939 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003940 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003941 author = None
3942 if options.contributor:
3943 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003944 hook_results = cl.RunHook(
3945 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003946 may_prompt=not options.force,
3947 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003948 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003949 if not hook_results.should_continue():
3950 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003951
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003952 # Check the tree status if the tree status URL is set.
3953 status = GetTreeStatus()
3954 if 'closed' == status:
3955 print('The tree is closed. Please wait for it to reopen. Use '
3956 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3957 return 1
3958 elif 'unknown' == status:
3959 print('Unable to determine tree status. Please verify manually and '
3960 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3961 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003962
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003963 change_desc = ChangeDescription(options.message)
3964 if not change_desc.description and cl.GetIssue():
3965 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003966
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003967 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00003968 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003969 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00003970 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003971 print('No description set.')
3972 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00003973 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003974
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003975 # Keep a separate copy for the commit message, because the commit message
3976 # contains the link to the Rietveld issue, while the Rietveld message contains
3977 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003978 # Keep a separate copy for the commit message.
3979 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00003980 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003981
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003982 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00003983 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00003984 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00003985 # after it. Add a period on a new line to circumvent this. Also add a space
3986 # before the period to make sure that Gitiles continues to correctly resolve
3987 # the URL.
3988 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003989 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003990 commit_desc.append_footer('Patch from %s.' % options.contributor)
3991
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00003992 print('Description:')
3993 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003994
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003995 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003996 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00003997 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003998
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003999 # We want to squash all this branch's commits into one commit with the proper
4000 # description. We do this by doing a "reset --soft" to the base branch (which
4001 # keeps the working copy the same), then dcommitting that. If origin/master
4002 # has a submodule merge commit, we'll also need to cherry-pick the squashed
4003 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004004 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004005 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4006 # Delete the branches if they exist.
4007 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4008 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4009 result = RunGitWithCode(showref_cmd)
4010 if result[0] == 0:
4011 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004012
4013 # We might be in a directory that's present in this branch but not in the
4014 # trunk. Move up to the top of the tree so that git commands that expect a
4015 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004016 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004017 if rel_base_path:
4018 os.chdir(rel_base_path)
4019
4020 # Stuff our change into the merge branch.
4021 # We wrap in a try...finally block so if anything goes wrong,
4022 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004023 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004024 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004025 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004026 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004027 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00004028 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004029 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004030 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004031 RunGit(
4032 [
4033 'commit', '--author', options.contributor,
4034 '-m', commit_desc.description,
4035 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004036 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004037 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004038 if base_has_submodules:
4039 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
4040 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
4041 RunGit(['checkout', CHERRY_PICK_BRANCH])
4042 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004043 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004044 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004045 mirror = settings.GetGitMirror(remote)
4046 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004047 pending_prefix = settings.GetPendingRefPrefix()
4048 if not pending_prefix or branch.startswith(pending_prefix):
4049 # If not using refs/pending/heads/* at all, or target ref is already set
4050 # to pending, then push to the target ref directly.
4051 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004052 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004053 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004054 else:
4055 # Cherry-pick the change on top of pending ref and then push it.
4056 assert branch.startswith('refs/'), branch
4057 assert pending_prefix[-1] == '/', pending_prefix
4058 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004059 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004060 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004061 if retcode == 0:
4062 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004063 else:
4064 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004065 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00004066 'svn', 'dcommit',
4067 '-C%s' % options.similarity,
4068 '--no-rebase', '--rmdir',
4069 ]
4070 if settings.GetForceHttpsCommitUrl():
4071 # Allow forcing https commit URLs for some projects that don't allow
4072 # committing to http URLs (like Google Code).
4073 remote_url = cl.GetGitSvnRemoteUrl()
4074 if urlparse.urlparse(remote_url).scheme == 'http':
4075 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004076 cmd_args.append('--commit-url=%s' % remote_url)
4077 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004078 if 'Committed r' in output:
4079 revision = re.match(
4080 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4081 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004082 finally:
4083 # And then swap back to the original branch and clean up.
4084 RunGit(['checkout', '-q', cl.GetBranch()])
4085 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004086 if base_has_submodules:
4087 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004088
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004089 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004090 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004091 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004092
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004093 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004094 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004095 try:
4096 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4097 # We set pushed_to_pending to False, since it made it all the way to the
4098 # real ref.
4099 pushed_to_pending = False
4100 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004101 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004102
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004103 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004104 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004105 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004106 if not to_pending:
4107 if viewvc_url and revision:
4108 change_desc.append_footer(
4109 'Committed: %s%s' % (viewvc_url, revision))
4110 elif revision:
4111 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004112 print('Closing issue '
4113 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004114 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004115 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004116 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004117 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004118 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004119 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004120 if options.bypass_hooks:
4121 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4122 else:
4123 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004124 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004125
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004126 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004127 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004128 print('The commit is in the pending queue (%s).' % pending_ref)
4129 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4130 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004131
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004132 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4133 if os.path.isfile(hook):
4134 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004135
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004136 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004137
4138
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004139def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004140 print()
4141 print('Waiting for commit to be landed on %s...' % real_ref)
4142 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004143 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4144 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004145 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004146
4147 loop = 0
4148 while True:
4149 sys.stdout.write('fetching (%d)... \r' % loop)
4150 sys.stdout.flush()
4151 loop += 1
4152
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004153 if mirror:
4154 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004155 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4156 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4157 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4158 for commit in commits.splitlines():
4159 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004160 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004161 return commit
4162
4163 current_rev = to_rev
4164
4165
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004166def PushToGitPending(remote, pending_ref, upstream_ref):
4167 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4168
4169 Returns:
4170 (retcode of last operation, output log of last operation).
4171 """
4172 assert pending_ref.startswith('refs/'), pending_ref
4173 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4174 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4175 code = 0
4176 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004177 max_attempts = 3
4178 attempts_left = max_attempts
4179 while attempts_left:
4180 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004181 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004182 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004183
4184 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004185 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004186 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004187 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004188 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004189 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004190 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004191 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004192 continue
4193
4194 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004195 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004196 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004197 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004198 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004199 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4200 'the following files have merge conflicts:' % pending_ref)
4201 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4202 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004203 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004204 return code, out
4205
4206 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004207 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004208 code, out = RunGitWithCode(
4209 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4210 if code == 0:
4211 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004212 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004213 return code, out
4214
vapiera7fbd5a2016-06-16 09:17:49 -07004215 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004216 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004217 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004218 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004219 print('Fatal push error. Make sure your .netrc credentials and git '
4220 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004221 return code, out
4222
vapiera7fbd5a2016-06-16 09:17:49 -07004223 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004224 return code, out
4225
4226
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004227def IsFatalPushFailure(push_stdout):
4228 """True if retrying push won't help."""
4229 return '(prohibited by Gerrit)' in push_stdout
4230
4231
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004232@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004233def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004234 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004235 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004236 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004237 # If it looks like previous commits were mirrored with git-svn.
4238 message = """This repository appears to be a git-svn mirror, but no
4239upstream SVN master is set. You probably need to run 'git auto-svn' once."""
4240 else:
4241 message = """This doesn't appear to be an SVN repository.
4242If your project has a true, writeable git repository, you probably want to run
4243'git cl land' instead.
4244If your project has a git mirror of an upstream SVN master, you probably need
4245to run 'git svn init'.
4246
4247Using the wrong command might cause your commit to appear to succeed, and the
4248review to be closed, without actually landing upstream. If you choose to
4249proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004250 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004251 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
tandrii3bb82ff2016-06-17 07:36:36 -07004252 # TODO(tandrii): kill this post SVN migration with
4253 # https://codereview.chromium.org/2076683002
4254 print('WARNING: chrome infrastructure is migrating SVN repos to Git.\n'
4255 'Please let us know of this project you are committing to:'
4256 ' http://crbug.com/600451')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004257 return SendUpstream(parser, args, 'dcommit')
4258
4259
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004260@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004261def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004262 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004263 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004264 print('This appears to be an SVN repository.')
4265 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004266 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004267 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004268 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004269
4270
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004271@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004272def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004273 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004274 parser.add_option('-b', dest='newbranch',
4275 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004276 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004277 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004278 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4279 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004280 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004281 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004282 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004283 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004284 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004285 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004286
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004287
4288 group = optparse.OptionGroup(
4289 parser,
4290 'Options for continuing work on the current issue uploaded from a '
4291 'different clone (e.g. different machine). Must be used independently '
4292 'from the other options. No issue number should be specified, and the '
4293 'branch must have an issue number associated with it')
4294 group.add_option('--reapply', action='store_true', dest='reapply',
4295 help='Reset the branch and reapply the issue.\n'
4296 'CAUTION: This will undo any local changes in this '
4297 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004298
4299 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004300 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004301 parser.add_option_group(group)
4302
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004303 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004304 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004305 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004306 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004307 auth_config = auth.extract_auth_config_from_options(options)
4308
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004309
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004310 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004311 if options.newbranch:
4312 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004313 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004314 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004315
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004316 cl = Changelist(auth_config=auth_config,
4317 codereview=options.forced_codereview)
4318 if not cl.GetIssue():
4319 parser.error('current branch must have an associated issue')
4320
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004321 upstream = cl.GetUpstreamBranch()
4322 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004323 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004324
4325 RunGit(['reset', '--hard', upstream])
4326 if options.pull:
4327 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004328
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004329 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4330 options.directory)
4331
4332 if len(args) != 1 or not args[0]:
4333 parser.error('Must specify issue number or url')
4334
4335 # We don't want uncommitted changes mixed up with the patch.
4336 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004337 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004338
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004339 if options.newbranch:
4340 if options.force:
4341 RunGit(['branch', '-D', options.newbranch],
4342 stderr=subprocess2.PIPE, error_ok=True)
4343 RunGit(['new-branch', options.newbranch])
4344
4345 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4346
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004347 if cl.IsGerrit():
4348 if options.reject:
4349 parser.error('--reject is not supported with Gerrit codereview.')
4350 if options.nocommit:
4351 parser.error('--nocommit is not supported with Gerrit codereview.')
4352 if options.directory:
4353 parser.error('--directory is not supported with Gerrit codereview.')
4354
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004355 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004356 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004357
4358
4359def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004360 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004361 # Provide a wrapper for git svn rebase to help avoid accidental
4362 # git svn dcommit.
4363 # It's the only command that doesn't use parser at all since we just defer
4364 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004365
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004366 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004367
4368
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004369def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004370 """Fetches the tree status and returns either 'open', 'closed',
4371 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004372 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004373 if url:
4374 status = urllib2.urlopen(url).read().lower()
4375 if status.find('closed') != -1 or status == '0':
4376 return 'closed'
4377 elif status.find('open') != -1 or status == '1':
4378 return 'open'
4379 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004380 return 'unset'
4381
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004382
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004383def GetTreeStatusReason():
4384 """Fetches the tree status from a json url and returns the message
4385 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004386 url = settings.GetTreeStatusUrl()
4387 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004388 connection = urllib2.urlopen(json_url)
4389 status = json.loads(connection.read())
4390 connection.close()
4391 return status['message']
4392
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004393
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004394def GetBuilderMaster(bot_list):
4395 """For a given builder, fetch the master from AE if available."""
4396 map_url = 'https://builders-map.appspot.com/'
4397 try:
4398 master_map = json.load(urllib2.urlopen(map_url))
4399 except urllib2.URLError as e:
4400 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4401 (map_url, e))
4402 except ValueError as e:
4403 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4404 if not master_map:
4405 return None, 'Failed to build master map.'
4406
4407 result_master = ''
4408 for bot in bot_list:
4409 builder = bot.split(':', 1)[0]
4410 master_list = master_map.get(builder, [])
4411 if not master_list:
4412 return None, ('No matching master for builder %s.' % builder)
4413 elif len(master_list) > 1:
4414 return None, ('The builder name %s exists in multiple masters %s.' %
4415 (builder, master_list))
4416 else:
4417 cur_master = master_list[0]
4418 if not result_master:
4419 result_master = cur_master
4420 elif result_master != cur_master:
4421 return None, 'The builders do not belong to the same master.'
4422 return result_master, None
4423
4424
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004425def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004426 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004427 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004428 status = GetTreeStatus()
4429 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004430 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004431 return 2
4432
vapiera7fbd5a2016-06-16 09:17:49 -07004433 print('The tree is %s' % status)
4434 print()
4435 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004436 if status != 'open':
4437 return 1
4438 return 0
4439
4440
maruel@chromium.org15192402012-09-06 12:38:29 +00004441def CMDtry(parser, args):
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004442 """Triggers try jobs through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00004443 group = optparse.OptionGroup(parser, "Try job options")
4444 group.add_option(
4445 "-b", "--bot", action="append",
4446 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
4447 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004448 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00004449 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004450 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00004451 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004452 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00004453 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004454 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004455 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004456 "-r", "--revision",
4457 help="Revision to use for the try job; default: the "
4458 "revision will be determined by the try server; see "
4459 "its waterfall for more info")
4460 group.add_option(
4461 "-c", "--clobber", action="store_true", default=False,
4462 help="Force a clobber before building; e.g. don't do an "
4463 "incremental build")
4464 group.add_option(
4465 "--project",
4466 help="Override which project to use. Projects are defined "
4467 "server-side to define what default bot set to use")
4468 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004469 "-p", "--property", dest="properties", action="append", default=[],
4470 help="Specify generic properties in the form -p key1=value1 -p "
4471 "key2=value2 etc (buildbucket only). The value will be treated as "
4472 "json if decodable, or as string otherwise.")
4473 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004474 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004475 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004476 "--use-rietveld", action="store_true", default=False,
4477 help="Use Rietveld to trigger try jobs.")
4478 group.add_option(
4479 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4480 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004481 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004482 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004483 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004484 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004485
machenbach@chromium.org45453142015-09-15 08:45:22 +00004486 if options.use_rietveld and options.properties:
4487 parser.error('Properties can only be specified with buildbucket')
4488
4489 # Make sure that all properties are prop=value pairs.
4490 bad_params = [x for x in options.properties if '=' not in x]
4491 if bad_params:
4492 parser.error('Got properties with missing "=": %s' % bad_params)
4493
maruel@chromium.org15192402012-09-06 12:38:29 +00004494 if args:
4495 parser.error('Unknown arguments: %s' % args)
4496
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004497 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004498 if not cl.GetIssue():
4499 parser.error('Need to upload first')
4500
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004501 if cl.IsGerrit():
4502 parser.error(
4503 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4504 'If your project has Commit Queue, dry run is a workaround:\n'
4505 ' git cl set-commit --dry-run')
4506 # Code below assumes Rietveld issue.
4507 # TODO(tandrii): actually implement for Gerrit http://crbug.com/599931.
4508
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004509 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004510 if props.get('closed'):
4511 parser.error('Cannot send tryjobs for a closed CL')
4512
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004513 if props.get('private'):
4514 parser.error('Cannot use trybots with private issue')
4515
maruel@chromium.org15192402012-09-06 12:38:29 +00004516 if not options.name:
4517 options.name = cl.GetBranch()
4518
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004519 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004520 options.master, err_msg = GetBuilderMaster(options.bot)
4521 if err_msg:
4522 parser.error('Tryserver master cannot be found because: %s\n'
4523 'Please manually specify the tryserver master'
4524 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004525
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004526 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004527 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004528 if not options.bot:
4529 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004530
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004531 # Get try masters from PRESUBMIT.py files.
4532 masters = presubmit_support.DoGetTryMasters(
4533 change,
4534 change.LocalPaths(),
4535 settings.GetRoot(),
4536 None,
4537 None,
4538 options.verbose,
4539 sys.stdout)
4540 if masters:
4541 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004542
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004543 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4544 options.bot = presubmit_support.DoGetTrySlaves(
4545 change,
4546 change.LocalPaths(),
4547 settings.GetRoot(),
4548 None,
4549 None,
4550 options.verbose,
4551 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004552
4553 if not options.bot:
4554 # Get try masters from cq.cfg if any.
4555 # TODO(tandrii): some (but very few) projects store cq.cfg in different
4556 # location.
4557 cq_cfg = os.path.join(change.RepositoryRoot(),
4558 'infra', 'config', 'cq.cfg')
4559 if os.path.exists(cq_cfg):
4560 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00004561 cq_masters = commit_queue.get_master_builder_map(
4562 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004563 for master, builders in cq_masters.iteritems():
4564 for builder in builders:
4565 # Skip presubmit builders, because these will fail without LGTM.
machenbach@chromium.org2403e802016-04-29 12:34:42 +00004566 masters.setdefault(master, {})[builder] = ['defaulttests']
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004567 if masters:
tandriib93dd2b2016-06-07 08:03:08 -07004568 print('Loaded default bots from CQ config (%s)' % cq_cfg)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004569 return masters
tandriib93dd2b2016-06-07 08:03:08 -07004570 else:
4571 print('CQ config exists (%s) but has no try bots listed' % cq_cfg)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004572
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004573 if not options.bot:
4574 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00004575
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004576 builders_and_tests = {}
4577 # TODO(machenbach): The old style command-line options don't support
4578 # multiple try masters yet.
4579 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4580 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4581
4582 for bot in old_style:
4583 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004584 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004585 elif ',' in bot:
4586 parser.error('Specify one bot per --bot flag')
4587 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004588 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004589
4590 for bot, tests in new_style:
4591 builders_and_tests.setdefault(bot, []).extend(tests)
4592
4593 # Return a master map with one master to be backwards compatible. The
4594 # master name defaults to an empty string, which will cause the master
4595 # not to be set on rietveld (deprecated).
4596 return {options.master: builders_and_tests}
4597
4598 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004599
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004600 for builders in masters.itervalues():
4601 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004602 print('ERROR You are trying to send a job to a triggered bot. This type '
4603 'of bot requires an\ninitial job from a parent (usually a builder).'
4604 ' Instead send your job to the parent.\n'
4605 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004606 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004607
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004608 patchset = cl.GetMostRecentPatchset()
4609 if patchset and patchset != cl.GetPatchset():
4610 print(
4611 '\nWARNING Mismatch between local config and server. Did a previous '
4612 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4613 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004614 if options.luci:
4615 trigger_luci_job(cl, masters, options)
4616 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004617 try:
4618 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4619 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004620 print('ERROR: %s' % ex)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004621 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004622 except Exception as e:
4623 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
vapiera7fbd5a2016-06-16 09:17:49 -07004624 print('ERROR: Exception when trying to trigger tryjobs: %s\n%s' %
4625 (e, stacktrace))
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004626 return 1
4627 else:
4628 try:
4629 cl.RpcServer().trigger_distributed_try_jobs(
4630 cl.GetIssue(), patchset, options.name, options.clobber,
4631 options.revision, masters)
4632 except urllib2.HTTPError as e:
4633 if e.code == 404:
4634 print('404 from rietveld; '
4635 'did you mean to use "git try" instead of "git cl try"?')
4636 return 1
4637 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004638
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004639 for (master, builders) in sorted(masters.iteritems()):
4640 if master:
vapiera7fbd5a2016-06-16 09:17:49 -07004641 print('Master: %s' % master)
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004642 length = max(len(builder) for builder in builders)
4643 for builder in sorted(builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004644 print(' %*s: %s' % (length, builder, ','.join(builders[builder])))
maruel@chromium.org15192402012-09-06 12:38:29 +00004645 return 0
4646
4647
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004648def CMDtry_results(parser, args):
4649 group = optparse.OptionGroup(parser, "Try job results options")
4650 group.add_option(
4651 "-p", "--patchset", type=int, help="patchset number if not current.")
4652 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004653 "--print-master", action='store_true', help="print master name as well.")
4654 group.add_option(
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004655 "--color", action='store_true', default=setup_color.IS_TTY,
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004656 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004657 group.add_option(
4658 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4659 help="Host of buildbucket. The default host is %default.")
4660 parser.add_option_group(group)
4661 auth.add_auth_options(parser)
4662 options, args = parser.parse_args(args)
4663 if args:
4664 parser.error('Unrecognized args: %s' % ' '.join(args))
4665
4666 auth_config = auth.extract_auth_config_from_options(options)
4667 cl = Changelist(auth_config=auth_config)
4668 if not cl.GetIssue():
4669 parser.error('Need to upload first')
4670
4671 if not options.patchset:
4672 options.patchset = cl.GetMostRecentPatchset()
4673 if options.patchset and options.patchset != cl.GetPatchset():
4674 print(
4675 '\nWARNING Mismatch between local config and server. Did a previous '
4676 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4677 'Continuing using\npatchset %s.\n' % options.patchset)
4678 try:
4679 jobs = fetch_try_jobs(auth_config, cl, options)
4680 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004681 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004682 return 1
4683 except Exception as e:
4684 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
vapiera7fbd5a2016-06-16 09:17:49 -07004685 print('ERROR: Exception when trying to fetch tryjobs: %s\n%s' %
4686 (e, stacktrace))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004687 return 1
4688 print_tryjobs(options, jobs)
4689 return 0
4690
4691
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004692@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004693def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004694 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004695 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004696 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004697 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004698
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004699 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004700 if args:
4701 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004702 branch = cl.GetBranch()
4703 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004704 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004705 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004706
4707 # Clear configured merge-base, if there is one.
4708 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004709 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004710 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004711 return 0
4712
4713
thestig@chromium.org00858c82013-12-02 23:08:03 +00004714def CMDweb(parser, args):
4715 """Opens the current CL in the web browser."""
4716 _, args = parser.parse_args(args)
4717 if args:
4718 parser.error('Unrecognized args: %s' % ' '.join(args))
4719
4720 issue_url = Changelist().GetIssueURL()
4721 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004722 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004723 return 1
4724
4725 webbrowser.open(issue_url)
4726 return 0
4727
4728
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004729def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004730 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004731 parser.add_option('-d', '--dry-run', action='store_true',
4732 help='trigger in dry run mode')
4733 parser.add_option('-c', '--clear', action='store_true',
4734 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004735 auth.add_auth_options(parser)
4736 options, args = parser.parse_args(args)
4737 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004738 if args:
4739 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004740 if options.dry_run and options.clear:
4741 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4742
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004743 cl = Changelist(auth_config=auth_config)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004744 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07004745 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004746 elif options.dry_run:
4747 state = _CQState.DRY_RUN
4748 else:
4749 state = _CQState.COMMIT
4750 if not cl.GetIssue():
4751 parser.error('Must upload the issue first')
tandrii4d0545a2016-07-06 03:56:49 -07004752 cl._codereview_impl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004753 return 0
4754
4755
groby@chromium.org411034a2013-02-26 15:12:01 +00004756def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004757 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004758 auth.add_auth_options(parser)
4759 options, args = parser.parse_args(args)
4760 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004761 if args:
4762 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004763 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00004764 # Ensure there actually is an issue to close.
4765 cl.GetDescription()
4766 cl.CloseIssue()
4767 return 0
4768
4769
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004770def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004771 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004772 auth.add_auth_options(parser)
4773 options, args = parser.parse_args(args)
4774 auth_config = auth.extract_auth_config_from_options(options)
4775 if args:
4776 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004777
4778 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004779 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004780 # Staged changes would be committed along with the patch from last
4781 # upload, hence counted toward the "last upload" side in the final
4782 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004783 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004784 return 1
4785
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004786 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004787 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004788 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004789 if not issue:
4790 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004791 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004792 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004793
4794 # Create a new branch based on the merge-base
4795 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004796 # Clear cached branch in cl object, to avoid overwriting original CL branch
4797 # properties.
4798 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004799 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004800 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004801 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004802 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004803 return rtn
4804
wychen@chromium.org06928532015-02-03 02:11:29 +00004805 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004806 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00004807 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004808 finally:
4809 RunGit(['checkout', '-q', branch])
4810 RunGit(['branch', '-D', TMP_BRANCH])
4811
4812 return 0
4813
4814
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004815def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004816 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004817 parser.add_option(
4818 '--no-color',
4819 action='store_true',
4820 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004821 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004822 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004823 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004824
4825 author = RunGit(['config', 'user.email']).strip() or None
4826
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004827 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004828
4829 if args:
4830 if len(args) > 1:
4831 parser.error('Unknown args')
4832 base_branch = args[0]
4833 else:
4834 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004835 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004836
4837 change = cl.GetChange(base_branch, None)
4838 return owners_finder.OwnersFinder(
4839 [f.LocalPath() for f in
4840 cl.GetChange(base_branch, None).AffectedFiles()],
4841 change.RepositoryRoot(), author,
4842 fopen=file, os_path=os.path, glob=glob.glob,
4843 disable_color=options.no_color).run()
4844
4845
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004846def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004847 """Generates a diff command."""
4848 # Generate diff for the current branch's changes.
4849 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4850 upstream_commit, '--' ]
4851
4852 if args:
4853 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004854 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004855 diff_cmd.append(arg)
4856 else:
4857 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004858
4859 return diff_cmd
4860
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004861def MatchingFileType(file_name, extensions):
4862 """Returns true if the file name ends with one of the given extensions."""
4863 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004864
enne@chromium.org555cfe42014-01-29 18:21:39 +00004865@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004866def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004867 """Runs auto-formatting tools (clang-format etc.) on the diff."""
zengsterbf470142016-07-07 16:43:00 -07004868 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07004869 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004870 parser.add_option('--full', action='store_true',
4871 help='Reformat the full content of all touched files')
4872 parser.add_option('--dry-run', action='store_true',
4873 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004874 parser.add_option('--python', action='store_true',
4875 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004876 parser.add_option('--diff', action='store_true',
4877 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004878 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004879
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004880 # git diff generates paths against the root of the repository. Change
4881 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004882 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004883 if rel_base_path:
4884 os.chdir(rel_base_path)
4885
digit@chromium.org29e47272013-05-17 17:01:46 +00004886 # Grab the merge-base commit, i.e. the upstream commit of the current
4887 # branch when it was created or the last time it was rebased. This is
4888 # to cover the case where the user may have called "git fetch origin",
4889 # moving the origin branch to a newer commit, but hasn't rebased yet.
4890 upstream_commit = None
4891 cl = Changelist()
4892 upstream_branch = cl.GetUpstreamBranch()
4893 if upstream_branch:
4894 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4895 upstream_commit = upstream_commit.strip()
4896
4897 if not upstream_commit:
4898 DieWithError('Could not find base commit for this branch. '
4899 'Are you in detached state?')
4900
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004901 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4902 diff_output = RunGit(changed_files_cmd)
4903 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004904 # Filter out files deleted by this CL
4905 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004906
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004907 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4908 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4909 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004910 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004911
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004912 top_dir = os.path.normpath(
4913 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
4914
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004915 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4916 # formatted. This is used to block during the presubmit.
4917 return_value = 0
4918
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004919 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004920 # Locate the clang-format binary in the checkout
4921 try:
4922 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07004923 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004924 DieWithError(e)
4925
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004926 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004927 cmd = [clang_format_tool]
4928 if not opts.dry_run and not opts.diff:
4929 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004930 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004931 if opts.diff:
4932 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004933 else:
4934 env = os.environ.copy()
4935 env['PATH'] = str(os.path.dirname(clang_format_tool))
4936 try:
4937 script = clang_format.FindClangFormatScriptInChromiumTree(
4938 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07004939 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004940 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004941
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004942 cmd = [sys.executable, script, '-p0']
4943 if not opts.dry_run and not opts.diff:
4944 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004945
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004946 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
4947 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004948
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004949 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
4950 if opts.diff:
4951 sys.stdout.write(stdout)
4952 if opts.dry_run and len(stdout) > 0:
4953 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004954
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004955 # Similar code to above, but using yapf on .py files rather than clang-format
4956 # on C/C++ files
4957 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004958 yapf_tool = gclient_utils.FindExecutable('yapf')
4959 if yapf_tool is None:
4960 DieWithError('yapf not found in PATH')
4961
4962 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004963 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004964 cmd = [yapf_tool]
4965 if not opts.dry_run and not opts.diff:
4966 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004967 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004968 if opts.diff:
4969 sys.stdout.write(stdout)
4970 else:
4971 # TODO(sbc): yapf --lines mode still has some issues.
4972 # https://github.com/google/yapf/issues/154
4973 DieWithError('--python currently only works with --full')
4974
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004975 # Dart's formatter does not have the nice property of only operating on
4976 # modified chunks, so hard code full.
4977 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004978 try:
4979 command = [dart_format.FindDartFmtToolInChromiumTree()]
4980 if not opts.dry_run and not opts.diff:
4981 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004982 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004983
ppi@chromium.org6593d932016-03-03 15:41:15 +00004984 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004985 if opts.dry_run and stdout:
4986 return_value = 2
4987 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07004988 print('Warning: Unable to check Dart code formatting. Dart SDK not '
4989 'found in this checkout. Files in other languages are still '
4990 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004991
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004992 # Format GN build files. Always run on full build files for canonical form.
4993 if gn_diff_files:
4994 cmd = ['gn', 'format']
4995 if not opts.dry_run and not opts.diff:
4996 cmd.append('--in-place')
4997 for gn_diff_file in gn_diff_files:
bsep@chromium.org627d9002016-04-29 00:00:52 +00004998 stdout = RunCommand(cmd + [gn_diff_file],
4999 shell=sys.platform == 'win32',
5000 cwd=top_dir)
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005001 if opts.diff:
5002 sys.stdout.write(stdout)
5003
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005004 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005005
5006
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005007@subcommand.usage('<codereview url or issue id>')
5008def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005009 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005010 _, args = parser.parse_args(args)
5011
5012 if len(args) != 1:
5013 parser.print_help()
5014 return 1
5015
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005016 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005017 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005018 parser.print_help()
5019 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005020 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005021
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005022 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005023 output = RunGit(['config', '--local', '--get-regexp',
5024 r'branch\..*\.%s' % issueprefix],
5025 error_ok=True)
5026 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005027 if issue == target_issue:
5028 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005029
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005030 branches = []
5031 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00005032 branches.extend(find_issues(cls.IssueSettingSuffix()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005033 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005034 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005035 return 1
5036 if len(branches) == 1:
5037 RunGit(['checkout', branches[0]])
5038 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005039 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005040 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005041 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005042 which = raw_input('Choose by index: ')
5043 try:
5044 RunGit(['checkout', branches[int(which)]])
5045 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005046 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005047 return 1
5048
5049 return 0
5050
5051
maruel@chromium.org29404b52014-09-08 22:58:00 +00005052def CMDlol(parser, args):
5053 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005054 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005055 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5056 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5057 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005058 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005059 return 0
5060
5061
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005062class OptionParser(optparse.OptionParser):
5063 """Creates the option parse and add --verbose support."""
5064 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005065 optparse.OptionParser.__init__(
5066 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005067 self.add_option(
5068 '-v', '--verbose', action='count', default=0,
5069 help='Use 2 times for more debugging info')
5070
5071 def parse_args(self, args=None, values=None):
5072 options, args = optparse.OptionParser.parse_args(self, args, values)
5073 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5074 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5075 return options, args
5076
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005077
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005078def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005079 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005080 print('\nYour python version %s is unsupported, please upgrade.\n' %
5081 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005082 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005083
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005084 # Reload settings.
5085 global settings
5086 settings = Settings()
5087
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005088 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005089 dispatcher = subcommand.CommandDispatcher(__name__)
5090 try:
5091 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005092 except auth.AuthenticationError as e:
5093 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005094 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005095 if e.code != 500:
5096 raise
5097 DieWithError(
5098 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5099 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005100 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005101
5102
5103if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005104 # These affect sys.stdout so do it outside of main() to simplify mocks in
5105 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005106 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005107 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005108 try:
5109 sys.exit(main(sys.argv[1:]))
5110 except KeyboardInterrupt:
5111 sys.stderr.write('interrupted\n')
5112 sys.exit(1)