blob: 6a59cda52b9aa21facb9041e22bd9b86135569a7 [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
sheyang@google.com6ebaf782015-05-12 19:17:54 +000016import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000017import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000018import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000019import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import optparse
21import os
22import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000023import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025import textwrap
sheyang@google.com6ebaf782015-05-12 19:17:54 +000026import time
27import traceback
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000028import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000030import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000031import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000032import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000033import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000034
35try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000036 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000037except ImportError:
38 pass
39
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000040from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000041from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000043import auth
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +000044from luci_hacks import trigger_luci_job as luci_trigger
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000045import clang_format
tandrii@chromium.org71184c02016-01-13 15:18:44 +000046import commit_queue
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000047import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000048import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000049import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000050import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000051import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000052import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000053import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000054import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000055import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000056import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000057import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000058import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000059import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000060import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000061import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000062import watchlists
63
tandrii7400cf02016-06-21 08:48:07 -070064__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000065
tandrii9d2c7a32016-06-22 03:42:45 -070066COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000067DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000068POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000069DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000070GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
rmistry@google.comc68112d2015-03-03 12:48:06 +000071REFS_THAT_ALIAS_TO_OTHER_REFS = {
72 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
73 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
74}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000075
thestig@chromium.org44202a22014-03-11 19:22:18 +000076# Valid extensions for files we want to lint.
77DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
78DEFAULT_LINT_IGNORE_REGEX = r"$^"
79
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000080# Shortcut since it quickly becomes redundant.
81Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000082
maruel@chromium.orgddd59412011-11-30 14:20:38 +000083# Initialized in main()
84settings = None
85
86
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000087def DieWithError(message):
vapiera7fbd5a2016-06-16 09:17:49 -070088 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000089 sys.exit(1)
90
91
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000092def GetNoGitPagerEnv():
93 env = os.environ.copy()
94 # 'cat' is a magical git string that disables pagers on all platforms.
95 env['GIT_PAGER'] = 'cat'
96 return env
97
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000098
bsep@chromium.org627d9002016-04-29 00:00:52 +000099def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000100 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000101 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000102 except subprocess2.CalledProcessError as e:
103 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000104 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000105 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000106 'Command "%s" failed.\n%s' % (
107 ' '.join(args), error_message or e.stdout or ''))
108 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000109
110
111def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000112 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000113 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000114
115
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000116def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000117 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000118 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000119 if suppress_stderr:
120 stderr = subprocess2.VOID
121 else:
122 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000123 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000124 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000125 stdout=subprocess2.PIPE,
126 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000127 return code, out[0]
128 except ValueError:
129 # When the subprocess fails, it returns None. That triggers a ValueError
130 # when trying to unpack the return value into (out, code).
131 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000132
133
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000134def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000135 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000136 return RunGitWithCode(args, suppress_stderr=True)[1]
137
138
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000139def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000140 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000141 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000142 return (version.startswith(prefix) and
143 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000144
145
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000146def BranchExists(branch):
147 """Return True if specified branch exists."""
148 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
149 suppress_stderr=True)
150 return not code
151
152
maruel@chromium.org90541732011-04-01 17:54:18 +0000153def ask_for_data(prompt):
154 try:
155 return raw_input(prompt)
156 except KeyboardInterrupt:
157 # Hide the exception.
158 sys.exit(1)
159
160
iannucci@chromium.org79540052012-10-19 23:15:26 +0000161def git_set_branch_value(key, value):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000162 branch = GetCurrentBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000163 if not branch:
164 return
165
166 cmd = ['config']
167 if isinstance(value, int):
168 cmd.append('--int')
169 git_key = 'branch.%s.%s' % (branch, key)
170 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000171
172
173def git_get_branch_default(key, default):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000174 branch = GetCurrentBranch()
iannucci@chromium.org79540052012-10-19 23:15:26 +0000175 if branch:
176 git_key = 'branch.%s.%s' % (branch, key)
177 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
178 try:
179 return int(stdout.strip())
180 except ValueError:
181 pass
182 return default
183
184
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000185def add_git_similarity(parser):
186 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000187 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000188 help='Sets the percentage that a pair of files need to match in order to'
189 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000190 parser.add_option(
191 '--find-copies', action='store_true',
192 help='Allows git to look for copies.')
193 parser.add_option(
194 '--no-find-copies', action='store_false', dest='find_copies',
195 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000196
197 old_parser_args = parser.parse_args
198 def Parse(args):
199 options, args = old_parser_args(args)
200
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000201 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000202 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000203 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000204 print('Note: Saving similarity of %d%% in git config.'
205 % options.similarity)
206 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000207
iannucci@chromium.org79540052012-10-19 23:15:26 +0000208 options.similarity = max(0, min(options.similarity, 100))
209
210 if options.find_copies is None:
211 options.find_copies = bool(
212 git_get_branch_default('git-find-copies', True))
213 else:
214 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000215
216 print('Using %d%% similarity for rename/copy detection. '
217 'Override with --similarity.' % options.similarity)
218
219 return options, args
220 parser.parse_args = Parse
221
222
machenbach@chromium.org45453142015-09-15 08:45:22 +0000223def _get_properties_from_options(options):
224 properties = dict(x.split('=', 1) for x in options.properties)
225 for key, val in properties.iteritems():
226 try:
227 properties[key] = json.loads(val)
228 except ValueError:
229 pass # If a value couldn't be evaluated, treat it as a string.
230 return properties
231
232
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000233def _prefix_master(master):
234 """Convert user-specified master name to full master name.
235
236 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
237 name, while the developers always use shortened master name
238 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
239 function does the conversion for buildbucket migration.
240 """
241 prefix = 'master.'
242 if master.startswith(prefix):
243 return master
244 return '%s%s' % (prefix, master)
245
246
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000247def _buildbucket_retry(operation_name, http, *args, **kwargs):
248 """Retries requests to buildbucket service and returns parsed json content."""
249 try_count = 0
250 while True:
251 response, content = http.request(*args, **kwargs)
252 try:
253 content_json = json.loads(content)
254 except ValueError:
255 content_json = None
256
257 # Buildbucket could return an error even if status==200.
258 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000259 error = content_json.get('error')
260 if error.get('code') == 403:
261 raise BuildbucketResponseException(
262 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000263 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000264 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000265 raise BuildbucketResponseException(msg)
266
267 if response.status == 200:
268 if not content_json:
269 raise BuildbucketResponseException(
270 'Buildbucket returns invalid json content: %s.\n'
271 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
272 content)
273 return content_json
274 if response.status < 500 or try_count >= 2:
275 raise httplib2.HttpLib2Error(content)
276
277 # status >= 500 means transient failures.
278 logging.debug('Transient errors when %s. Will retry.', operation_name)
279 time.sleep(0.5 + 1.5*try_count)
280 try_count += 1
281 assert False, 'unreachable'
282
283
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000284def trigger_luci_job(changelist, masters, options):
285 """Send a job to run on LUCI."""
286 issue_props = changelist.GetIssueProperties()
287 issue = changelist.GetIssue()
288 patchset = changelist.GetMostRecentPatchset()
289 for builders_and_tests in sorted(masters.itervalues()):
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000290 # TODO(hinoka et al): add support for other properties.
291 # Currently, this completely ignores testfilter and other properties.
292 for builder in sorted(builders_and_tests):
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000293 luci_trigger.trigger(
294 builder, 'HEAD', issue, patchset, issue_props['project'])
295
296
machenbach@chromium.org45453142015-09-15 08:45:22 +0000297def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000298 rietveld_url = settings.GetDefaultServerUrl()
299 rietveld_host = urlparse.urlparse(rietveld_url).hostname
300 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
301 http = authenticator.authorize(httplib2.Http())
302 http.force_exception_to_status_code = True
303 issue_props = changelist.GetIssueProperties()
304 issue = changelist.GetIssue()
305 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000306 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000307
308 buildbucket_put_url = (
309 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000310 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000311 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
312 hostname=rietveld_host,
313 issue=issue,
314 patch=patchset)
315
316 batch_req_body = {'builds': []}
317 print_text = []
318 print_text.append('Tried jobs on:')
319 for master, builders_and_tests in sorted(masters.iteritems()):
320 print_text.append('Master: %s' % master)
321 bucket = _prefix_master(master)
322 for builder, tests in sorted(builders_and_tests.iteritems()):
323 print_text.append(' %s: %s' % (builder, tests))
324 parameters = {
325 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000326 'changes': [{
327 'author': {'email': issue_props['owner_email']},
328 'revision': options.revision,
329 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000330 'properties': {
331 'category': category,
332 'issue': issue,
333 'master': master,
334 'patch_project': issue_props['project'],
335 'patch_storage': 'rietveld',
336 'patchset': patchset,
337 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000338 'rietveld': rietveld_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000339 },
340 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000341 if 'presubmit' in builder.lower():
342 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000343 if tests:
344 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000345 if properties:
346 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000347 if options.clobber:
348 parameters['properties']['clobber'] = True
349 batch_req_body['builds'].append(
350 {
351 'bucket': bucket,
352 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000353 'client_operation_id': str(uuid.uuid4()),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000354 'tags': ['builder:%s' % builder,
355 'buildset:%s' % buildset,
356 'master:%s' % master,
357 'user_agent:git_cl_try']
358 }
359 )
360
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000361 _buildbucket_retry(
362 'triggering tryjobs',
363 http,
364 buildbucket_put_url,
365 'PUT',
366 body=json.dumps(batch_req_body),
367 headers={'Content-Type': 'application/json'}
368 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000369 print_text.append('To see results here, run: git cl try-results')
370 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700371 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000372
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000373
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000374def fetch_try_jobs(auth_config, changelist, options):
375 """Fetches tryjobs from buildbucket.
376
377 Returns a map from build id to build info as json dictionary.
378 """
379 rietveld_url = settings.GetDefaultServerUrl()
380 rietveld_host = urlparse.urlparse(rietveld_url).hostname
381 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
382 if authenticator.has_cached_credentials():
383 http = authenticator.authorize(httplib2.Http())
384 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700385 print('Warning: Some results might be missing because %s' %
386 # Get the message on how to login.
387 (auth.LoginRequiredError(rietveld_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000388 http = httplib2.Http()
389
390 http.force_exception_to_status_code = True
391
392 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
393 hostname=rietveld_host,
394 issue=changelist.GetIssue(),
395 patch=options.patchset)
396 params = {'tag': 'buildset:%s' % buildset}
397
398 builds = {}
399 while True:
400 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
401 hostname=options.buildbucket_host,
402 params=urllib.urlencode(params))
403 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
404 for build in content.get('builds', []):
405 builds[build['id']] = build
406 if 'next_cursor' in content:
407 params['start_cursor'] = content['next_cursor']
408 else:
409 break
410 return builds
411
412
413def print_tryjobs(options, builds):
414 """Prints nicely result of fetch_try_jobs."""
415 if not builds:
vapiera7fbd5a2016-06-16 09:17:49 -0700416 print('No tryjobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000417 return
418
419 # Make a copy, because we'll be modifying builds dictionary.
420 builds = builds.copy()
421 builder_names_cache = {}
422
423 def get_builder(b):
424 try:
425 return builder_names_cache[b['id']]
426 except KeyError:
427 try:
428 parameters = json.loads(b['parameters_json'])
429 name = parameters['builder_name']
430 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700431 print('WARNING: failed to get builder name for build %s: %s' % (
432 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000433 name = None
434 builder_names_cache[b['id']] = name
435 return name
436
437 def get_bucket(b):
438 bucket = b['bucket']
439 if bucket.startswith('master.'):
440 return bucket[len('master.'):]
441 return bucket
442
443 if options.print_master:
444 name_fmt = '%%-%ds %%-%ds' % (
445 max(len(str(get_bucket(b))) for b in builds.itervalues()),
446 max(len(str(get_builder(b))) for b in builds.itervalues()))
447 def get_name(b):
448 return name_fmt % (get_bucket(b), get_builder(b))
449 else:
450 name_fmt = '%%-%ds' % (
451 max(len(str(get_builder(b))) for b in builds.itervalues()))
452 def get_name(b):
453 return name_fmt % get_builder(b)
454
455 def sort_key(b):
456 return b['status'], b.get('result'), get_name(b), b.get('url')
457
458 def pop(title, f, color=None, **kwargs):
459 """Pop matching builds from `builds` dict and print them."""
460
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000461 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000462 colorize = str
463 else:
464 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
465
466 result = []
467 for b in builds.values():
468 if all(b.get(k) == v for k, v in kwargs.iteritems()):
469 builds.pop(b['id'])
470 result.append(b)
471 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700472 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000473 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700474 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000475
476 total = len(builds)
477 pop(status='COMPLETED', result='SUCCESS',
478 title='Successes:', color=Fore.GREEN,
479 f=lambda b: (get_name(b), b.get('url')))
480 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
481 title='Infra Failures:', color=Fore.MAGENTA,
482 f=lambda b: (get_name(b), b.get('url')))
483 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
484 title='Failures:', color=Fore.RED,
485 f=lambda b: (get_name(b), b.get('url')))
486 pop(status='COMPLETED', result='CANCELED',
487 title='Canceled:', color=Fore.MAGENTA,
488 f=lambda b: (get_name(b),))
489 pop(status='COMPLETED', result='FAILURE',
490 failure_reason='INVALID_BUILD_DEFINITION',
491 title='Wrong master/builder name:', color=Fore.MAGENTA,
492 f=lambda b: (get_name(b),))
493 pop(status='COMPLETED', result='FAILURE',
494 title='Other failures:',
495 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
496 pop(status='COMPLETED',
497 title='Other finished:',
498 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
499 pop(status='STARTED',
500 title='Started:', color=Fore.YELLOW,
501 f=lambda b: (get_name(b), b.get('url')))
502 pop(status='SCHEDULED',
503 title='Scheduled:',
504 f=lambda b: (get_name(b), 'id=%s' % b['id']))
505 # The last section is just in case buildbucket API changes OR there is a bug.
506 pop(title='Other:',
507 f=lambda b: (get_name(b), 'id=%s' % b['id']))
508 assert len(builds) == 0
vapiera7fbd5a2016-06-16 09:17:49 -0700509 print('Total: %d tryjobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000510
511
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000512def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
513 """Return the corresponding git ref if |base_url| together with |glob_spec|
514 matches the full |url|.
515
516 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
517 """
518 fetch_suburl, as_ref = glob_spec.split(':')
519 if allow_wildcards:
520 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
521 if glob_match:
522 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
523 # "branches/{472,597,648}/src:refs/remotes/svn/*".
524 branch_re = re.escape(base_url)
525 if glob_match.group(1):
526 branch_re += '/' + re.escape(glob_match.group(1))
527 wildcard = glob_match.group(2)
528 if wildcard == '*':
529 branch_re += '([^/]*)'
530 else:
531 # Escape and replace surrounding braces with parentheses and commas
532 # with pipe symbols.
533 wildcard = re.escape(wildcard)
534 wildcard = re.sub('^\\\\{', '(', wildcard)
535 wildcard = re.sub('\\\\,', '|', wildcard)
536 wildcard = re.sub('\\\\}$', ')', wildcard)
537 branch_re += wildcard
538 if glob_match.group(3):
539 branch_re += re.escape(glob_match.group(3))
540 match = re.match(branch_re, url)
541 if match:
542 return re.sub('\*$', match.group(1), as_ref)
543
544 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
545 if fetch_suburl:
546 full_url = base_url + '/' + fetch_suburl
547 else:
548 full_url = base_url
549 if full_url == url:
550 return as_ref
551 return None
552
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000553
iannucci@chromium.org79540052012-10-19 23:15:26 +0000554def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000555 """Prints statistics about the change to the user."""
556 # --no-ext-diff is broken in some versions of Git, so try to work around
557 # this by overriding the environment (but there is still a problem if the
558 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000559 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000560 if 'GIT_EXTERNAL_DIFF' in env:
561 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000562
563 if find_copies:
564 similarity_options = ['--find-copies-harder', '-l100000',
565 '-C%s' % similarity]
566 else:
567 similarity_options = ['-M%s' % similarity]
568
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000569 try:
570 stdout = sys.stdout.fileno()
571 except AttributeError:
572 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000573 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000574 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000575 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000576 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000577
578
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000579class BuildbucketResponseException(Exception):
580 pass
581
582
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000583class Settings(object):
584 def __init__(self):
585 self.default_server = None
586 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000587 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000588 self.is_git_svn = None
589 self.svn_branch = None
590 self.tree_status_url = None
591 self.viewvc_url = None
592 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000593 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000594 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000595 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000596 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000597 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000598 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000599 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000600
601 def LazyUpdateIfNeeded(self):
602 """Updates the settings from a codereview.settings file, if available."""
603 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000604 # The only value that actually changes the behavior is
605 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000606 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000607 error_ok=True
608 ).strip().lower()
609
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000610 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000611 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000612 LoadCodereviewSettingsFromFile(cr_settings_file)
613 self.updated = True
614
615 def GetDefaultServerUrl(self, error_ok=False):
616 if not self.default_server:
617 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000618 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000619 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000620 if error_ok:
621 return self.default_server
622 if not self.default_server:
623 error_message = ('Could not find settings file. You must configure '
624 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000625 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000626 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000627 return self.default_server
628
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000629 @staticmethod
630 def GetRelativeRoot():
631 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000632
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000633 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000634 if self.root is None:
635 self.root = os.path.abspath(self.GetRelativeRoot())
636 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000637
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000638 def GetGitMirror(self, remote='origin'):
639 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000640 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000641 if not os.path.isdir(local_url):
642 return None
643 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
644 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
645 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
646 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
647 if mirror.exists():
648 return mirror
649 return None
650
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000651 def GetIsGitSvn(self):
652 """Return true if this repo looks like it's using git-svn."""
653 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000654 if self.GetPendingRefPrefix():
655 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
656 self.is_git_svn = False
657 else:
658 # If you have any "svn-remote.*" config keys, we think you're using svn.
659 self.is_git_svn = RunGitWithCode(
660 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000661 return self.is_git_svn
662
663 def GetSVNBranch(self):
664 if self.svn_branch is None:
665 if not self.GetIsGitSvn():
666 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
667
668 # Try to figure out which remote branch we're based on.
669 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000670 # 1) iterate through our branch history and find the svn URL.
671 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000672
673 # regexp matching the git-svn line that contains the URL.
674 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
675
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000676 # We don't want to go through all of history, so read a line from the
677 # pipe at a time.
678 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000679 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000680 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
681 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000682 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000683 for line in proc.stdout:
684 match = git_svn_re.match(line)
685 if match:
686 url = match.group(1)
687 proc.stdout.close() # Cut pipe.
688 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000689
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000690 if url:
691 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
692 remotes = RunGit(['config', '--get-regexp',
693 r'^svn-remote\..*\.url']).splitlines()
694 for remote in remotes:
695 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000696 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000697 remote = match.group(1)
698 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000699 rewrite_root = RunGit(
700 ['config', 'svn-remote.%s.rewriteRoot' % remote],
701 error_ok=True).strip()
702 if rewrite_root:
703 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000704 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000705 ['config', 'svn-remote.%s.fetch' % remote],
706 error_ok=True).strip()
707 if fetch_spec:
708 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
709 if self.svn_branch:
710 break
711 branch_spec = RunGit(
712 ['config', 'svn-remote.%s.branches' % remote],
713 error_ok=True).strip()
714 if branch_spec:
715 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
716 if self.svn_branch:
717 break
718 tag_spec = RunGit(
719 ['config', 'svn-remote.%s.tags' % remote],
720 error_ok=True).strip()
721 if tag_spec:
722 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
723 if self.svn_branch:
724 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000725
726 if not self.svn_branch:
727 DieWithError('Can\'t guess svn branch -- try specifying it on the '
728 'command line')
729
730 return self.svn_branch
731
732 def GetTreeStatusUrl(self, error_ok=False):
733 if not self.tree_status_url:
734 error_message = ('You must configure your tree status URL by running '
735 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000736 self.tree_status_url = self._GetRietveldConfig(
737 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000738 return self.tree_status_url
739
740 def GetViewVCUrl(self):
741 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000742 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000743 return self.viewvc_url
744
rmistry@google.com90752582014-01-14 21:04:50 +0000745 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000746 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000747
rmistry@google.com78948ed2015-07-08 23:09:57 +0000748 def GetIsSkipDependencyUpload(self, branch_name):
749 """Returns true if specified branch should skip dep uploads."""
750 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
751 error_ok=True)
752
rmistry@google.com5626a922015-02-26 14:03:30 +0000753 def GetRunPostUploadHook(self):
754 run_post_upload_hook = self._GetRietveldConfig(
755 'run-post-upload-hook', error_ok=True)
756 return run_post_upload_hook == "True"
757
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000758 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000759 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000760
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000761 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000762 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000763
ukai@chromium.orge8077812012-02-03 03:41:46 +0000764 def GetIsGerrit(self):
765 """Return true if this repo is assosiated with gerrit code review system."""
766 if self.is_gerrit is None:
767 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
768 return self.is_gerrit
769
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000770 def GetSquashGerritUploads(self):
771 """Return true if uploads to Gerrit should be squashed by default."""
772 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700773 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
774 if self.squash_gerrit_uploads is None:
775 # Default is squash now (http://crbug.com/611892#c23).
776 self.squash_gerrit_uploads = not (
777 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
778 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000779 return self.squash_gerrit_uploads
780
tandriia60502f2016-06-20 02:01:53 -0700781 def GetSquashGerritUploadsOverride(self):
782 """Return True or False if codereview.settings should be overridden.
783
784 Returns None if no override has been defined.
785 """
786 # See also http://crbug.com/611892#c23
787 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
788 error_ok=True).strip()
789 if result == 'true':
790 return True
791 if result == 'false':
792 return False
793 return None
794
tandrii@chromium.org28253532016-04-14 13:46:56 +0000795 def GetGerritSkipEnsureAuthenticated(self):
796 """Return True if EnsureAuthenticated should not be done for Gerrit
797 uploads."""
798 if self.gerrit_skip_ensure_authenticated is None:
799 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000800 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000801 error_ok=True).strip() == 'true')
802 return self.gerrit_skip_ensure_authenticated
803
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000804 def GetGitEditor(self):
805 """Return the editor specified in the git config, or None if none is."""
806 if self.git_editor is None:
807 self.git_editor = self._GetConfig('core.editor', error_ok=True)
808 return self.git_editor or None
809
thestig@chromium.org44202a22014-03-11 19:22:18 +0000810 def GetLintRegex(self):
811 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
812 DEFAULT_LINT_REGEX)
813
814 def GetLintIgnoreRegex(self):
815 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
816 DEFAULT_LINT_IGNORE_REGEX)
817
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000818 def GetProject(self):
819 if not self.project:
820 self.project = self._GetRietveldConfig('project', error_ok=True)
821 return self.project
822
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000823 def GetForceHttpsCommitUrl(self):
824 if not self.force_https_commit_url:
825 self.force_https_commit_url = self._GetRietveldConfig(
826 'force-https-commit-url', error_ok=True)
827 return self.force_https_commit_url
828
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000829 def GetPendingRefPrefix(self):
830 if not self.pending_ref_prefix:
831 self.pending_ref_prefix = self._GetRietveldConfig(
832 'pending-ref-prefix', error_ok=True)
833 return self.pending_ref_prefix
834
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000835 def _GetRietveldConfig(self, param, **kwargs):
836 return self._GetConfig('rietveld.' + param, **kwargs)
837
rmistry@google.com78948ed2015-07-08 23:09:57 +0000838 def _GetBranchConfig(self, branch_name, param, **kwargs):
839 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
840
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000841 def _GetConfig(self, param, **kwargs):
842 self.LazyUpdateIfNeeded()
843 return RunGit(['config', param], **kwargs).strip()
844
845
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000846def ShortBranchName(branch):
847 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000848 return branch.replace('refs/heads/', '', 1)
849
850
851def GetCurrentBranchRef():
852 """Returns branch ref (e.g., refs/heads/master) or None."""
853 return RunGit(['symbolic-ref', 'HEAD'],
854 stderr=subprocess2.VOID, error_ok=True).strip() or None
855
856
857def GetCurrentBranch():
858 """Returns current branch or None.
859
860 For refs/heads/* branches, returns just last part. For others, full ref.
861 """
862 branchref = GetCurrentBranchRef()
863 if branchref:
864 return ShortBranchName(branchref)
865 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000866
867
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000868class _CQState(object):
869 """Enum for states of CL with respect to Commit Queue."""
870 NONE = 'none'
871 DRY_RUN = 'dry_run'
872 COMMIT = 'commit'
873
874 ALL_STATES = [NONE, DRY_RUN, COMMIT]
875
876
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000877class _ParsedIssueNumberArgument(object):
878 def __init__(self, issue=None, patchset=None, hostname=None):
879 self.issue = issue
880 self.patchset = patchset
881 self.hostname = hostname
882
883 @property
884 def valid(self):
885 return self.issue is not None
886
887
888class _RietveldParsedIssueNumberArgument(_ParsedIssueNumberArgument):
889 def __init__(self, *args, **kwargs):
890 self.patch_url = kwargs.pop('patch_url', None)
891 super(_RietveldParsedIssueNumberArgument, self).__init__(*args, **kwargs)
892
893
894def ParseIssueNumberArgument(arg):
895 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
896 fail_result = _ParsedIssueNumberArgument()
897
898 if arg.isdigit():
899 return _ParsedIssueNumberArgument(issue=int(arg))
900 if not arg.startswith('http'):
901 return fail_result
902 url = gclient_utils.UpgradeToHttps(arg)
903 try:
904 parsed_url = urlparse.urlparse(url)
905 except ValueError:
906 return fail_result
907 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
908 tmp = cls.ParseIssueURL(parsed_url)
909 if tmp is not None:
910 return tmp
911 return fail_result
912
913
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000914class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000915 """Changelist works with one changelist in local branch.
916
917 Supports two codereview backends: Rietveld or Gerrit, selected at object
918 creation.
919
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +0000920 Notes:
921 * Not safe for concurrent multi-{thread,process} use.
922 * Caches values from current branch. Therefore, re-use after branch change
923 with care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000924 """
925
926 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
927 """Create a new ChangeList instance.
928
929 If issue is given, the codereview must be given too.
930
931 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
932 Otherwise, it's decided based on current configuration of the local branch,
933 with default being 'rietveld' for backwards compatibility.
934 See _load_codereview_impl for more details.
935
936 **kwargs will be passed directly to codereview implementation.
937 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000938 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000939 global settings
940 if not settings:
941 # Happens when git_cl.py is used as a utility library.
942 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000943
944 if issue:
945 assert codereview, 'codereview must be known, if issue is known'
946
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000947 self.branchref = branchref
948 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000949 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000950 self.branch = ShortBranchName(self.branchref)
951 else:
952 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000953 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000954 self.lookedup_issue = False
955 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000956 self.has_description = False
957 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000958 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000959 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000960 self.cc = None
961 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000962 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000963
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000964 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000965 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000966 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000967 assert self._codereview_impl
968 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000969
970 def _load_codereview_impl(self, codereview=None, **kwargs):
971 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000972 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
973 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
974 self._codereview = codereview
975 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000976 return
977
978 # Automatic selection based on issue number set for a current branch.
979 # Rietveld takes precedence over Gerrit.
980 assert not self.issue
981 # Whether we find issue or not, we are doing the lookup.
982 self.lookedup_issue = True
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000983 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000984 setting = cls.IssueSetting(self.GetBranch())
985 issue = RunGit(['config', setting], error_ok=True).strip()
986 if issue:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000987 self._codereview = codereview
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000988 self._codereview_impl = cls(self, **kwargs)
989 self.issue = int(issue)
990 return
991
992 # No issue is set for this branch, so decide based on repo-wide settings.
993 return self._load_codereview_impl(
994 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
995 **kwargs)
996
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000997 def IsGerrit(self):
998 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000999
1000 def GetCCList(self):
1001 """Return the users cc'd on this CL.
1002
1003 Return is a string suitable for passing to gcl with the --cc flag.
1004 """
1005 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001006 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001007 more_cc = ','.join(self.watchers)
1008 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1009 return self.cc
1010
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001011 def GetCCListWithoutDefault(self):
1012 """Return the users cc'd on this CL excluding default ones."""
1013 if self.cc is None:
1014 self.cc = ','.join(self.watchers)
1015 return self.cc
1016
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001017 def SetWatchers(self, watchers):
1018 """Set the list of email addresses that should be cc'd based on the changed
1019 files in this CL.
1020 """
1021 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001022
1023 def GetBranch(self):
1024 """Returns the short branch name, e.g. 'master'."""
1025 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001026 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001027 if not branchref:
1028 return None
1029 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001030 self.branch = ShortBranchName(self.branchref)
1031 return self.branch
1032
1033 def GetBranchRef(self):
1034 """Returns the full branch name, e.g. 'refs/heads/master'."""
1035 self.GetBranch() # Poke the lazy loader.
1036 return self.branchref
1037
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001038 def ClearBranch(self):
1039 """Clears cached branch data of this object."""
1040 self.branch = self.branchref = None
1041
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001042 @staticmethod
1043 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001044 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001045 e.g. 'origin', 'refs/heads/master'
1046 """
1047 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001048 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
1049 error_ok=True).strip()
1050 if upstream_branch:
1051 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
1052 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001053 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1054 error_ok=True).strip()
1055 if upstream_branch:
1056 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001057 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001058 # Fall back on trying a git-svn upstream branch.
1059 if settings.GetIsGitSvn():
1060 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001061 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001062 # Else, try to guess the origin remote.
1063 remote_branches = RunGit(['branch', '-r']).split()
1064 if 'origin/master' in remote_branches:
1065 # Fall back on origin/master if it exits.
1066 remote = 'origin'
1067 upstream_branch = 'refs/heads/master'
1068 elif 'origin/trunk' in remote_branches:
1069 # Fall back on origin/trunk if it exists. Generally a shared
1070 # git-svn clone
1071 remote = 'origin'
1072 upstream_branch = 'refs/heads/trunk'
1073 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001074 DieWithError(
1075 'Unable to determine default branch to diff against.\n'
1076 'Either pass complete "git diff"-style arguments, like\n'
1077 ' git cl upload origin/master\n'
1078 'or verify this branch is set up to track another \n'
1079 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001080
1081 return remote, upstream_branch
1082
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001083 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001084 upstream_branch = self.GetUpstreamBranch()
1085 if not BranchExists(upstream_branch):
1086 DieWithError('The upstream for the current branch (%s) does not exist '
1087 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001088 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001089 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001090
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001091 def GetUpstreamBranch(self):
1092 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001093 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001094 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001095 upstream_branch = upstream_branch.replace('refs/heads/',
1096 'refs/remotes/%s/' % remote)
1097 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1098 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001099 self.upstream_branch = upstream_branch
1100 return self.upstream_branch
1101
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001102 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001103 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001104 remote, branch = None, self.GetBranch()
1105 seen_branches = set()
1106 while branch not in seen_branches:
1107 seen_branches.add(branch)
1108 remote, branch = self.FetchUpstreamTuple(branch)
1109 branch = ShortBranchName(branch)
1110 if remote != '.' or branch.startswith('refs/remotes'):
1111 break
1112 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001113 remotes = RunGit(['remote'], error_ok=True).split()
1114 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001115 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001116 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001117 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001118 logging.warning('Could not determine which remote this change is '
1119 'associated with, so defaulting to "%s". This may '
1120 'not be what you want. You may prevent this message '
1121 'by running "git svn info" as documented here: %s',
1122 self._remote,
1123 GIT_INSTRUCTIONS_URL)
1124 else:
1125 logging.warn('Could not determine which remote this change is '
1126 'associated with. You may prevent this message by '
1127 'running "git svn info" as documented here: %s',
1128 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001129 branch = 'HEAD'
1130 if branch.startswith('refs/remotes'):
1131 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001132 elif branch.startswith('refs/branch-heads/'):
1133 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001134 else:
1135 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001136 return self._remote
1137
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001138 def GitSanityChecks(self, upstream_git_obj):
1139 """Checks git repo status and ensures diff is from local commits."""
1140
sbc@chromium.org79706062015-01-14 21:18:12 +00001141 if upstream_git_obj is None:
1142 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001143 print('ERROR: unable to determine current branch (detached HEAD?)',
1144 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001145 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001146 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001147 return False
1148
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001149 # Verify the commit we're diffing against is in our current branch.
1150 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1151 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1152 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001153 print('ERROR: %s is not in the current branch. You may need to rebase '
1154 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001155 return False
1156
1157 # List the commits inside the diff, and verify they are all local.
1158 commits_in_diff = RunGit(
1159 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1160 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1161 remote_branch = remote_branch.strip()
1162 if code != 0:
1163 _, remote_branch = self.GetRemoteBranch()
1164
1165 commits_in_remote = RunGit(
1166 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1167
1168 common_commits = set(commits_in_diff) & set(commits_in_remote)
1169 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001170 print('ERROR: Your diff contains %d commits already in %s.\n'
1171 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1172 'the diff. If you are using a custom git flow, you can override'
1173 ' the reference used for this check with "git config '
1174 'gitcl.remotebranch <git-ref>".' % (
1175 len(common_commits), remote_branch, upstream_git_obj),
1176 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001177 return False
1178 return True
1179
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001180 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001181 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001182
1183 Returns None if it is not set.
1184 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001185 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
1186 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001187
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001188 def GetGitSvnRemoteUrl(self):
1189 """Return the configured git-svn remote URL parsed from git svn info.
1190
1191 Returns None if it is not set.
1192 """
1193 # URL is dependent on the current directory.
1194 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1195 if data:
1196 keys = dict(line.split(': ', 1) for line in data.splitlines()
1197 if ': ' in line)
1198 return keys.get('URL', None)
1199 return None
1200
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001201 def GetRemoteUrl(self):
1202 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1203
1204 Returns None if there is no remote.
1205 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001206 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001207 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1208
1209 # If URL is pointing to a local directory, it is probably a git cache.
1210 if os.path.isdir(url):
1211 url = RunGit(['config', 'remote.%s.url' % remote],
1212 error_ok=True,
1213 cwd=url).strip()
1214 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001215
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001216 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001217 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001218 if self.issue is None and not self.lookedup_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001219 issue = RunGit(['config',
1220 self._codereview_impl.IssueSetting(self.GetBranch())],
1221 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001222 self.issue = int(issue) or None if issue else None
1223 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001224 return self.issue
1225
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001226 def GetIssueURL(self):
1227 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001228 issue = self.GetIssue()
1229 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001230 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001231 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001232
1233 def GetDescription(self, pretty=False):
1234 if not self.has_description:
1235 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001236 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001237 self.has_description = True
1238 if pretty:
1239 wrapper = textwrap.TextWrapper()
1240 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1241 return wrapper.fill(self.description)
1242 return self.description
1243
1244 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001245 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001246 if self.patchset is None and not self.lookedup_patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001247 patchset = RunGit(['config', self._codereview_impl.PatchsetSetting()],
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001248 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001249 self.patchset = int(patchset) or None if patchset else None
1250 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001251 return self.patchset
1252
1253 def SetPatchset(self, patchset):
1254 """Set this branch's patchset. If patchset=0, clears the patchset."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001255 patchset_setting = self._codereview_impl.PatchsetSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001256 if patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001257 RunGit(['config', patchset_setting, str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001258 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001259 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001260 RunGit(['config', '--unset', patchset_setting],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001261 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001262 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001263
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001264 def SetIssue(self, issue=None):
1265 """Set this branch's issue. If issue isn't given, clears the issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001266 issue_setting = self._codereview_impl.IssueSetting(self.GetBranch())
1267 codereview_setting = self._codereview_impl.GetCodereviewServerSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001268 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001269 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001270 RunGit(['config', issue_setting, str(issue)])
1271 codereview_server = self._codereview_impl.GetCodereviewServer()
1272 if codereview_server:
1273 RunGit(['config', codereview_setting, codereview_server])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001274 else:
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001275 # Reset it regardless. It doesn't hurt.
1276 config_settings = [issue_setting, self._codereview_impl.PatchsetSetting()]
1277 for prop in (['last-upload-hash'] +
1278 self._codereview_impl._PostUnsetIssueProperties()):
1279 config_settings.append('branch.%s.%s' % (self.GetBranch(), prop))
1280 for setting in config_settings:
1281 RunGit(['config', '--unset', setting], error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001282 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001283 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001284
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001285 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001286 if not self.GitSanityChecks(upstream_branch):
1287 DieWithError('\nGit sanity check failure')
1288
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001289 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001290 if not root:
1291 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001292 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001293
1294 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001295 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001296 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001297 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001298 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001299 except subprocess2.CalledProcessError:
1300 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001301 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001302 'This branch probably doesn\'t exist anymore. To reset the\n'
1303 'tracking branch, please run\n'
1304 ' git branch --set-upstream %s trunk\n'
1305 'replacing trunk with origin/master or the relevant branch') %
1306 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001307
maruel@chromium.org52424302012-08-29 15:14:30 +00001308 issue = self.GetIssue()
1309 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001310 if issue:
1311 description = self.GetDescription()
1312 else:
1313 # If the change was never uploaded, use the log messages of all commits
1314 # up to the branch point, as git cl upload will prefill the description
1315 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001316 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1317 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001318
1319 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001320 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001321 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001322 name,
1323 description,
1324 absroot,
1325 files,
1326 issue,
1327 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001328 author,
1329 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001330
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001331 def UpdateDescription(self, description):
1332 self.description = description
1333 return self._codereview_impl.UpdateDescriptionRemote(description)
1334
1335 def RunHook(self, committing, may_prompt, verbose, change):
1336 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1337 try:
1338 return presubmit_support.DoPresubmitChecks(change, committing,
1339 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1340 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001341 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1342 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001343 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001344 DieWithError(
1345 ('%s\nMaybe your depot_tools is out of date?\n'
1346 'If all fails, contact maruel@') % e)
1347
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001348 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1349 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001350 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1351 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001352 else:
1353 # Assume url.
1354 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1355 urlparse.urlparse(issue_arg))
1356 if not parsed_issue_arg or not parsed_issue_arg.valid:
1357 DieWithError('Failed to parse issue argument "%s". '
1358 'Must be an issue number or a valid URL.' % issue_arg)
1359 return self._codereview_impl.CMDPatchWithParsedIssue(
1360 parsed_issue_arg, reject, nocommit, directory)
1361
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001362 def CMDUpload(self, options, git_diff_args, orig_args):
1363 """Uploads a change to codereview."""
1364 if git_diff_args:
1365 # TODO(ukai): is it ok for gerrit case?
1366 base_branch = git_diff_args[0]
1367 else:
1368 if self.GetBranch() is None:
1369 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1370
1371 # Default to diffing against common ancestor of upstream branch
1372 base_branch = self.GetCommonAncestorWithUpstream()
1373 git_diff_args = [base_branch, 'HEAD']
1374
1375 # Make sure authenticated to codereview before running potentially expensive
1376 # hooks. It is a fast, best efforts check. Codereview still can reject the
1377 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001378 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001379
1380 # Apply watchlists on upload.
1381 change = self.GetChange(base_branch, None)
1382 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1383 files = [f.LocalPath() for f in change.AffectedFiles()]
1384 if not options.bypass_watchlists:
1385 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1386
1387 if not options.bypass_hooks:
1388 if options.reviewers or options.tbr_owners:
1389 # Set the reviewer list now so that presubmit checks can access it.
1390 change_description = ChangeDescription(change.FullDescriptionText())
1391 change_description.update_reviewers(options.reviewers,
1392 options.tbr_owners,
1393 change)
1394 change.SetDescriptionText(change_description.description)
1395 hook_results = self.RunHook(committing=False,
1396 may_prompt=not options.force,
1397 verbose=options.verbose,
1398 change=change)
1399 if not hook_results.should_continue():
1400 return 1
1401 if not options.reviewers and hook_results.reviewers:
1402 options.reviewers = hook_results.reviewers.split(',')
1403
1404 if self.GetIssue():
1405 latest_patchset = self.GetMostRecentPatchset()
1406 local_patchset = self.GetPatchset()
1407 if (latest_patchset and local_patchset and
1408 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001409 print('The last upload made from this repository was patchset #%d but '
1410 'the most recent patchset on the server is #%d.'
1411 % (local_patchset, latest_patchset))
1412 print('Uploading will still work, but if you\'ve uploaded to this '
1413 'issue from another machine or branch the patch you\'re '
1414 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001415 ask_for_data('About to upload; enter to confirm.')
1416
1417 print_stats(options.similarity, options.find_copies, git_diff_args)
1418 ret = self.CMDUploadChange(options, git_diff_args, change)
1419 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001420 if options.use_commit_queue:
1421 self.SetCQState(_CQState.COMMIT)
1422 elif options.cq_dry_run:
1423 self.SetCQState(_CQState.DRY_RUN)
1424
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001425 git_set_branch_value('last-upload-hash',
1426 RunGit(['rev-parse', 'HEAD']).strip())
1427 # Run post upload hooks, if specified.
1428 if settings.GetRunPostUploadHook():
1429 presubmit_support.DoPostUploadExecuter(
1430 change,
1431 self,
1432 settings.GetRoot(),
1433 options.verbose,
1434 sys.stdout)
1435
1436 # Upload all dependencies if specified.
1437 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001438 print()
1439 print('--dependencies has been specified.')
1440 print('All dependent local branches will be re-uploaded.')
1441 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001442 # Remove the dependencies flag from args so that we do not end up in a
1443 # loop.
1444 orig_args.remove('--dependencies')
1445 ret = upload_branch_deps(self, orig_args)
1446 return ret
1447
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001448 def SetCQState(self, new_state):
1449 """Update the CQ state for latest patchset.
1450
1451 Issue must have been already uploaded and known.
1452 """
1453 assert new_state in _CQState.ALL_STATES
1454 assert self.GetIssue()
1455 return self._codereview_impl.SetCQState(new_state)
1456
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001457 # Forward methods to codereview specific implementation.
1458
1459 def CloseIssue(self):
1460 return self._codereview_impl.CloseIssue()
1461
1462 def GetStatus(self):
1463 return self._codereview_impl.GetStatus()
1464
1465 def GetCodereviewServer(self):
1466 return self._codereview_impl.GetCodereviewServer()
1467
1468 def GetApprovingReviewers(self):
1469 return self._codereview_impl.GetApprovingReviewers()
1470
1471 def GetMostRecentPatchset(self):
1472 return self._codereview_impl.GetMostRecentPatchset()
1473
1474 def __getattr__(self, attr):
1475 # This is because lots of untested code accesses Rietveld-specific stuff
1476 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001477 # on a case by case basis.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001478 return getattr(self._codereview_impl, attr)
1479
1480
1481class _ChangelistCodereviewBase(object):
1482 """Abstract base class encapsulating codereview specifics of a changelist."""
1483 def __init__(self, changelist):
1484 self._changelist = changelist # instance of Changelist
1485
1486 def __getattr__(self, attr):
1487 # Forward methods to changelist.
1488 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1489 # _RietveldChangelistImpl to avoid this hack?
1490 return getattr(self._changelist, attr)
1491
1492 def GetStatus(self):
1493 """Apply a rough heuristic to give a simple summary of an issue's review
1494 or CQ status, assuming adherence to a common workflow.
1495
1496 Returns None if no issue for this branch, or specific string keywords.
1497 """
1498 raise NotImplementedError()
1499
1500 def GetCodereviewServer(self):
1501 """Returns server URL without end slash, like "https://codereview.com"."""
1502 raise NotImplementedError()
1503
1504 def FetchDescription(self):
1505 """Fetches and returns description from the codereview server."""
1506 raise NotImplementedError()
1507
1508 def GetCodereviewServerSetting(self):
1509 """Returns git config setting for the codereview server."""
1510 raise NotImplementedError()
1511
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001512 @classmethod
1513 def IssueSetting(cls, branch):
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001514 return 'branch.%s.%s' % (branch, cls.IssueSettingSuffix())
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001515
1516 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001517 def IssueSettingSuffix(cls):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001518 """Returns name of git config setting which stores issue number for a given
1519 branch."""
1520 raise NotImplementedError()
1521
1522 def PatchsetSetting(self):
1523 """Returns name of git config setting which stores issue number."""
1524 raise NotImplementedError()
1525
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001526 def _PostUnsetIssueProperties(self):
1527 """Which branch-specific properties to erase when unsettin issue."""
1528 raise NotImplementedError()
1529
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001530 def GetRieveldObjForPresubmit(self):
1531 # This is an unfortunate Rietveld-embeddedness in presubmit.
1532 # For non-Rietveld codereviews, this probably should return a dummy object.
1533 raise NotImplementedError()
1534
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001535 def GetGerritObjForPresubmit(self):
1536 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1537 return None
1538
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001539 def UpdateDescriptionRemote(self, description):
1540 """Update the description on codereview site."""
1541 raise NotImplementedError()
1542
1543 def CloseIssue(self):
1544 """Closes the issue."""
1545 raise NotImplementedError()
1546
1547 def GetApprovingReviewers(self):
1548 """Returns a list of reviewers approving the change.
1549
1550 Note: not necessarily committers.
1551 """
1552 raise NotImplementedError()
1553
1554 def GetMostRecentPatchset(self):
1555 """Returns the most recent patchset number from the codereview site."""
1556 raise NotImplementedError()
1557
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001558 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1559 directory):
1560 """Fetches and applies the issue.
1561
1562 Arguments:
1563 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1564 reject: if True, reject the failed patch instead of switching to 3-way
1565 merge. Rietveld only.
1566 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1567 only.
1568 directory: switch to directory before applying the patch. Rietveld only.
1569 """
1570 raise NotImplementedError()
1571
1572 @staticmethod
1573 def ParseIssueURL(parsed_url):
1574 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1575 failed."""
1576 raise NotImplementedError()
1577
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001578 def EnsureAuthenticated(self, force):
1579 """Best effort check that user is authenticated with codereview server.
1580
1581 Arguments:
1582 force: whether to skip confirmation questions.
1583 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001584 raise NotImplementedError()
1585
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001586 def CMDUploadChange(self, options, args, change):
1587 """Uploads a change to codereview."""
1588 raise NotImplementedError()
1589
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001590 def SetCQState(self, new_state):
1591 """Update the CQ state for latest patchset.
1592
1593 Issue must have been already uploaded and known.
1594 """
1595 raise NotImplementedError()
1596
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001597
1598class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1599 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1600 super(_RietveldChangelistImpl, self).__init__(changelist)
1601 assert settings, 'must be initialized in _ChangelistCodereviewBase'
martiniss6eda05f2016-06-30 10:18:35 -07001602 if not rietveld_server:
1603 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001604
1605 self._rietveld_server = rietveld_server
1606 self._auth_config = auth_config
1607 self._props = None
1608 self._rpc_server = None
1609
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001610 def GetCodereviewServer(self):
1611 if not self._rietveld_server:
1612 # If we're on a branch then get the server potentially associated
1613 # with that branch.
1614 if self.GetIssue():
1615 rietveld_server_setting = self.GetCodereviewServerSetting()
1616 if rietveld_server_setting:
1617 self._rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1618 ['config', rietveld_server_setting], error_ok=True).strip())
1619 if not self._rietveld_server:
1620 self._rietveld_server = settings.GetDefaultServerUrl()
1621 return self._rietveld_server
1622
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001623 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001624 """Best effort check that user is authenticated with Rietveld server."""
1625 if self._auth_config.use_oauth2:
1626 authenticator = auth.get_authenticator_for_host(
1627 self.GetCodereviewServer(), self._auth_config)
1628 if not authenticator.has_cached_credentials():
1629 raise auth.LoginRequiredError(self.GetCodereviewServer())
1630
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001631 def FetchDescription(self):
1632 issue = self.GetIssue()
1633 assert issue
1634 try:
1635 return self.RpcServer().get_description(issue).strip()
1636 except urllib2.HTTPError as e:
1637 if e.code == 404:
1638 DieWithError(
1639 ('\nWhile fetching the description for issue %d, received a '
1640 '404 (not found)\n'
1641 'error. It is likely that you deleted this '
1642 'issue on the server. If this is the\n'
1643 'case, please run\n\n'
1644 ' git cl issue 0\n\n'
1645 'to clear the association with the deleted issue. Then run '
1646 'this command again.') % issue)
1647 else:
1648 DieWithError(
1649 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1650 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001651 print('Warning: Failed to retrieve CL description due to network '
1652 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001653 return ''
1654
1655 def GetMostRecentPatchset(self):
1656 return self.GetIssueProperties()['patchsets'][-1]
1657
1658 def GetPatchSetDiff(self, issue, patchset):
1659 return self.RpcServer().get(
1660 '/download/issue%s_%s.diff' % (issue, patchset))
1661
1662 def GetIssueProperties(self):
1663 if self._props is None:
1664 issue = self.GetIssue()
1665 if not issue:
1666 self._props = {}
1667 else:
1668 self._props = self.RpcServer().get_issue_properties(issue, True)
1669 return self._props
1670
1671 def GetApprovingReviewers(self):
1672 return get_approving_reviewers(self.GetIssueProperties())
1673
1674 def AddComment(self, message):
1675 return self.RpcServer().add_comment(self.GetIssue(), message)
1676
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001677 def GetStatus(self):
1678 """Apply a rough heuristic to give a simple summary of an issue's review
1679 or CQ status, assuming adherence to a common workflow.
1680
1681 Returns None if no issue for this branch, or one of the following keywords:
1682 * 'error' - error from review tool (including deleted issues)
1683 * 'unsent' - not sent for review
1684 * 'waiting' - waiting for review
1685 * 'reply' - waiting for owner to reply to review
1686 * 'lgtm' - LGTM from at least one approved reviewer
1687 * 'commit' - in the commit queue
1688 * 'closed' - closed
1689 """
1690 if not self.GetIssue():
1691 return None
1692
1693 try:
1694 props = self.GetIssueProperties()
1695 except urllib2.HTTPError:
1696 return 'error'
1697
1698 if props.get('closed'):
1699 # Issue is closed.
1700 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001701 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001702 # Issue is in the commit queue.
1703 return 'commit'
1704
1705 try:
1706 reviewers = self.GetApprovingReviewers()
1707 except urllib2.HTTPError:
1708 return 'error'
1709
1710 if reviewers:
1711 # Was LGTM'ed.
1712 return 'lgtm'
1713
1714 messages = props.get('messages') or []
1715
tandrii9d2c7a32016-06-22 03:42:45 -07001716 # Skip CQ messages that don't require owner's action.
1717 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1718 if 'Dry run:' in messages[-1]['text']:
1719 messages.pop()
1720 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1721 # This message always follows prior messages from CQ,
1722 # so skip this too.
1723 messages.pop()
1724 else:
1725 # This is probably a CQ messages warranting user attention.
1726 break
1727
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001728 if not messages:
1729 # No message was sent.
1730 return 'unsent'
1731 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07001732 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001733 return 'reply'
1734 return 'waiting'
1735
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001736 def UpdateDescriptionRemote(self, description):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001737 return self.RpcServer().update_description(
1738 self.GetIssue(), self.description)
1739
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001740 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001741 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001742
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001743 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07001744 return self.SetFlags({flag: value})
1745
1746 def SetFlags(self, flags):
1747 """Sets flags on this CL/patchset in Rietveld.
1748
1749 The latest patchset in Rietveld must be the same as latest known locally.
1750 """
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001751 if not self.GetPatchset():
1752 DieWithError('The patchset needs to match. Send another patchset.')
1753 try:
tandrii4b233bd2016-07-06 03:50:29 -07001754 return self.RpcServer().set_flags(
1755 self.GetIssue(), self.GetPatchset(), flags)
vapierfd77ac72016-06-16 08:33:57 -07001756 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001757 if e.code == 404:
1758 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1759 if e.code == 403:
1760 DieWithError(
1761 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1762 'match?') % (self.GetIssue(), self.GetPatchset()))
1763 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001764
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001765 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001766 """Returns an upload.RpcServer() to access this review's rietveld instance.
1767 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001768 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001769 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001770 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001771 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001772 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001773
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001774 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001775 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001776 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001777
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001778 def PatchsetSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001779 """Return the git setting that stores this change's most recent patchset."""
1780 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1781
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001782 def GetCodereviewServerSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001783 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001784 branch = self.GetBranch()
1785 if branch:
1786 return 'branch.%s.rietveldserver' % branch
1787 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001788
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001789 def _PostUnsetIssueProperties(self):
1790 """Which branch-specific properties to erase when unsetting issue."""
1791 return ['rietveldserver']
1792
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001793 def GetRieveldObjForPresubmit(self):
1794 return self.RpcServer()
1795
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001796 def SetCQState(self, new_state):
1797 props = self.GetIssueProperties()
1798 if props.get('private'):
1799 DieWithError('Cannot set-commit on private issue')
1800
1801 if new_state == _CQState.COMMIT:
1802 self.SetFlag('commit', '1')
1803 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07001804 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001805 else:
tandrii4b233bd2016-07-06 03:50:29 -07001806 assert new_state == _CQState.DRY_RUN
1807 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001808
1809
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001810 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1811 directory):
1812 # TODO(maruel): Use apply_issue.py
1813
1814 # PatchIssue should never be called with a dirty tree. It is up to the
1815 # caller to check this, but just in case we assert here since the
1816 # consequences of the caller not checking this could be dire.
1817 assert(not git_common.is_dirty_git_tree('apply'))
1818 assert(parsed_issue_arg.valid)
1819 self._changelist.issue = parsed_issue_arg.issue
1820 if parsed_issue_arg.hostname:
1821 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1822
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001823 if (isinstance(parsed_issue_arg, _RietveldParsedIssueNumberArgument) and
1824 parsed_issue_arg.patch_url):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001825 assert parsed_issue_arg.patchset
1826 patchset = parsed_issue_arg.patchset
1827 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1828 else:
1829 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1830 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1831
1832 # Switch up to the top-level directory, if necessary, in preparation for
1833 # applying the patch.
1834 top = settings.GetRelativeRoot()
1835 if top:
1836 os.chdir(top)
1837
1838 # Git patches have a/ at the beginning of source paths. We strip that out
1839 # with a sed script rather than the -p flag to patch so we can feed either
1840 # Git or svn-style patches into the same apply command.
1841 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1842 try:
1843 patch_data = subprocess2.check_output(
1844 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1845 except subprocess2.CalledProcessError:
1846 DieWithError('Git patch mungling failed.')
1847 logging.info(patch_data)
1848
1849 # We use "git apply" to apply the patch instead of "patch" so that we can
1850 # pick up file adds.
1851 # The --index flag means: also insert into the index (so we catch adds).
1852 cmd = ['git', 'apply', '--index', '-p0']
1853 if directory:
1854 cmd.extend(('--directory', directory))
1855 if reject:
1856 cmd.append('--reject')
1857 elif IsGitVersionAtLeast('1.7.12'):
1858 cmd.append('--3way')
1859 try:
1860 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
1861 stdin=patch_data, stdout=subprocess2.VOID)
1862 except subprocess2.CalledProcessError:
vapiera7fbd5a2016-06-16 09:17:49 -07001863 print('Failed to apply the patch')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001864 return 1
1865
1866 # If we had an issue, commit the current state and register the issue.
1867 if not nocommit:
1868 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
1869 'patch from issue %(i)s at patchset '
1870 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
1871 % {'i': self.GetIssue(), 'p': patchset})])
1872 self.SetIssue(self.GetIssue())
1873 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07001874 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001875 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001876 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001877 return 0
1878
1879 @staticmethod
1880 def ParseIssueURL(parsed_url):
1881 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1882 return None
1883 # Typical url: https://domain/<issue_number>[/[other]]
1884 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
1885 if match:
1886 return _RietveldParsedIssueNumberArgument(
1887 issue=int(match.group(1)),
1888 hostname=parsed_url.netloc)
1889 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
1890 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
1891 if match:
1892 return _RietveldParsedIssueNumberArgument(
1893 issue=int(match.group(1)),
1894 patchset=int(match.group(2)),
1895 hostname=parsed_url.netloc,
1896 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
1897 return None
1898
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001899 def CMDUploadChange(self, options, args, change):
1900 """Upload the patch to Rietveld."""
1901 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1902 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001903 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
1904 if options.emulate_svn_auto_props:
1905 upload_args.append('--emulate_svn_auto_props')
1906
1907 change_desc = None
1908
1909 if options.email is not None:
1910 upload_args.extend(['--email', options.email])
1911
1912 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07001913 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001914 upload_args.extend(['--title', options.title])
1915 if options.message:
1916 upload_args.extend(['--message', options.message])
1917 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07001918 print('This branch is associated with issue %s. '
1919 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001920 else:
nodirca166002016-06-27 10:59:51 -07001921 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001922 upload_args.extend(['--title', options.title])
1923 message = (options.title or options.message or
1924 CreateDescriptionFromLog(args))
1925 change_desc = ChangeDescription(message)
1926 if options.reviewers or options.tbr_owners:
1927 change_desc.update_reviewers(options.reviewers,
1928 options.tbr_owners,
1929 change)
1930 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07001931 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001932
1933 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07001934 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001935 return 1
1936
1937 upload_args.extend(['--message', change_desc.description])
1938 if change_desc.get_reviewers():
1939 upload_args.append('--reviewers=%s' % ','.join(
1940 change_desc.get_reviewers()))
1941 if options.send_mail:
1942 if not change_desc.get_reviewers():
1943 DieWithError("Must specify reviewers to send email.")
1944 upload_args.append('--send_mail')
1945
1946 # We check this before applying rietveld.private assuming that in
1947 # rietveld.cc only addresses which we can send private CLs to are listed
1948 # if rietveld.private is set, and so we should ignore rietveld.cc only
1949 # when --private is specified explicitly on the command line.
1950 if options.private:
1951 logging.warn('rietveld.cc is ignored since private flag is specified. '
1952 'You need to review and add them manually if necessary.')
1953 cc = self.GetCCListWithoutDefault()
1954 else:
1955 cc = self.GetCCList()
1956 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
1957 if cc:
1958 upload_args.extend(['--cc', cc])
1959
1960 if options.private or settings.GetDefaultPrivateFlag() == "True":
1961 upload_args.append('--private')
1962
1963 upload_args.extend(['--git_similarity', str(options.similarity)])
1964 if not options.find_copies:
1965 upload_args.extend(['--git_no_find_copies'])
1966
1967 # Include the upstream repo's URL in the change -- this is useful for
1968 # projects that have their source spread across multiple repos.
1969 remote_url = self.GetGitBaseUrlFromConfig()
1970 if not remote_url:
1971 if settings.GetIsGitSvn():
1972 remote_url = self.GetGitSvnRemoteUrl()
1973 else:
1974 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
1975 remote_url = '%s@%s' % (self.GetRemoteUrl(),
1976 self.GetUpstreamBranch().split('/')[-1])
1977 if remote_url:
1978 upload_args.extend(['--base_url', remote_url])
1979 remote, remote_branch = self.GetRemoteBranch()
1980 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
1981 settings.GetPendingRefPrefix())
1982 if target_ref:
1983 upload_args.extend(['--target_ref', target_ref])
1984
1985 # Look for dependent patchsets. See crbug.com/480453 for more details.
1986 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
1987 upstream_branch = ShortBranchName(upstream_branch)
1988 if remote is '.':
1989 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001990 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001991 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07001992 print()
1993 print('Skipping dependency patchset upload because git config '
1994 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
1995 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001996 else:
1997 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001998 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001999 auth_config=auth_config)
2000 branch_cl_issue_url = branch_cl.GetIssueURL()
2001 branch_cl_issue = branch_cl.GetIssue()
2002 branch_cl_patchset = branch_cl.GetPatchset()
2003 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2004 upload_args.extend(
2005 ['--depends_on_patchset', '%s:%s' % (
2006 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002007 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002008 '\n'
2009 'The current branch (%s) is tracking a local branch (%s) with '
2010 'an associated CL.\n'
2011 'Adding %s/#ps%s as a dependency patchset.\n'
2012 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2013 branch_cl_patchset))
2014
2015 project = settings.GetProject()
2016 if project:
2017 upload_args.extend(['--project', project])
2018
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002019 try:
2020 upload_args = ['upload'] + upload_args + args
2021 logging.info('upload.RealMain(%s)', upload_args)
2022 issue, patchset = upload.RealMain(upload_args)
2023 issue = int(issue)
2024 patchset = int(patchset)
2025 except KeyboardInterrupt:
2026 sys.exit(1)
2027 except:
2028 # If we got an exception after the user typed a description for their
2029 # change, back up the description before re-raising.
2030 if change_desc:
2031 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2032 print('\nGot exception while uploading -- saving description to %s\n' %
2033 backup_path)
2034 backup_file = open(backup_path, 'w')
2035 backup_file.write(change_desc.description)
2036 backup_file.close()
2037 raise
2038
2039 if not self.GetIssue():
2040 self.SetIssue(issue)
2041 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002042 return 0
2043
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002044
2045class _GerritChangelistImpl(_ChangelistCodereviewBase):
2046 def __init__(self, changelist, auth_config=None):
2047 # auth_config is Rietveld thing, kept here to preserve interface only.
2048 super(_GerritChangelistImpl, self).__init__(changelist)
2049 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002050 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002051 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002052 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002053
2054 def _GetGerritHost(self):
2055 # Lazy load of configs.
2056 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002057 if self._gerrit_host and '.' not in self._gerrit_host:
2058 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2059 # This happens for internal stuff http://crbug.com/614312.
2060 parsed = urlparse.urlparse(self.GetRemoteUrl())
2061 if parsed.scheme == 'sso':
2062 print('WARNING: using non https URLs for remote is likely broken\n'
2063 ' Your current remote is: %s' % self.GetRemoteUrl())
2064 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2065 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002066 return self._gerrit_host
2067
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002068 def _GetGitHost(self):
2069 """Returns git host to be used when uploading change to Gerrit."""
2070 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2071
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002072 def GetCodereviewServer(self):
2073 if not self._gerrit_server:
2074 # If we're on a branch then get the server potentially associated
2075 # with that branch.
2076 if self.GetIssue():
2077 gerrit_server_setting = self.GetCodereviewServerSetting()
2078 if gerrit_server_setting:
2079 self._gerrit_server = RunGit(['config', gerrit_server_setting],
2080 error_ok=True).strip()
2081 if self._gerrit_server:
2082 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
2083 if not self._gerrit_server:
2084 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2085 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002086 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002087 parts[0] = parts[0] + '-review'
2088 self._gerrit_host = '.'.join(parts)
2089 self._gerrit_server = 'https://%s' % self._gerrit_host
2090 return self._gerrit_server
2091
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002092 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00002093 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002094 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002095
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002096 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002097 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002098 if settings.GetGerritSkipEnsureAuthenticated():
2099 # For projects with unusual authentication schemes.
2100 # See http://crbug.com/603378.
2101 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002102 # Lazy-loader to identify Gerrit and Git hosts.
2103 if gerrit_util.GceAuthenticator.is_gce():
2104 return
2105 self.GetCodereviewServer()
2106 git_host = self._GetGitHost()
2107 assert self._gerrit_server and self._gerrit_host
2108 cookie_auth = gerrit_util.CookiesAuthenticator()
2109
2110 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2111 git_auth = cookie_auth.get_auth_header(git_host)
2112 if gerrit_auth and git_auth:
2113 if gerrit_auth == git_auth:
2114 return
2115 print((
2116 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2117 ' Check your %s or %s file for credentials of hosts:\n'
2118 ' %s\n'
2119 ' %s\n'
2120 ' %s') %
2121 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2122 git_host, self._gerrit_host,
2123 cookie_auth.get_new_password_message(git_host)))
2124 if not force:
2125 ask_for_data('If you know what you are doing, press Enter to continue, '
2126 'Ctrl+C to abort.')
2127 return
2128 else:
2129 missing = (
2130 [] if gerrit_auth else [self._gerrit_host] +
2131 [] if git_auth else [git_host])
2132 DieWithError('Credentials for the following hosts are required:\n'
2133 ' %s\n'
2134 'These are read from %s (or legacy %s)\n'
2135 '%s' % (
2136 '\n '.join(missing),
2137 cookie_auth.get_gitcookies_path(),
2138 cookie_auth.get_netrc_path(),
2139 cookie_auth.get_new_password_message(git_host)))
2140
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002141
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002142 def PatchsetSetting(self):
2143 """Return the git setting that stores this change's most recent patchset."""
2144 return 'branch.%s.gerritpatchset' % self.GetBranch()
2145
2146 def GetCodereviewServerSetting(self):
2147 """Returns the git setting that stores this change's Gerrit server."""
2148 branch = self.GetBranch()
2149 if branch:
2150 return 'branch.%s.gerritserver' % branch
2151 return None
2152
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002153 def _PostUnsetIssueProperties(self):
2154 """Which branch-specific properties to erase when unsetting issue."""
2155 return [
2156 'gerritserver',
2157 'gerritsquashhash',
2158 ]
2159
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002160 def GetRieveldObjForPresubmit(self):
2161 class ThisIsNotRietveldIssue(object):
2162 def __nonzero__(self):
2163 # This is a hack to make presubmit_support think that rietveld is not
2164 # defined, yet still ensure that calls directly result in a decent
2165 # exception message below.
2166 return False
2167
2168 def __getattr__(self, attr):
2169 print(
2170 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2171 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2172 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2173 'or use Rietveld for codereview.\n'
2174 'See also http://crbug.com/579160.' % attr)
2175 raise NotImplementedError()
2176 return ThisIsNotRietveldIssue()
2177
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002178 def GetGerritObjForPresubmit(self):
2179 return presubmit_support.GerritAccessor(self._GetGerritHost())
2180
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002181 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002182 """Apply a rough heuristic to give a simple summary of an issue's review
2183 or CQ status, assuming adherence to a common workflow.
2184
2185 Returns None if no issue for this branch, or one of the following keywords:
2186 * 'error' - error from review tool (including deleted issues)
2187 * 'unsent' - no reviewers added
2188 * 'waiting' - waiting for review
2189 * 'reply' - waiting for owner to reply to review
2190 * 'not lgtm' - Code-Review -2 from at least one approved reviewer
2191 * 'lgtm' - Code-Review +2 from at least one approved reviewer
2192 * 'commit' - in the commit queue
2193 * 'closed' - abandoned
2194 """
2195 if not self.GetIssue():
2196 return None
2197
2198 try:
2199 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
2200 except httplib.HTTPException:
2201 return 'error'
2202
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002203 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002204 return 'closed'
2205
2206 cq_label = data['labels'].get('Commit-Queue', {})
2207 if cq_label:
2208 # Vote value is a stringified integer, which we expect from 0 to 2.
2209 vote_value = cq_label.get('value', '0')
2210 vote_text = cq_label.get('values', {}).get(vote_value, '')
2211 if vote_text.lower() == 'commit':
2212 return 'commit'
2213
2214 lgtm_label = data['labels'].get('Code-Review', {})
2215 if lgtm_label:
2216 if 'rejected' in lgtm_label:
2217 return 'not lgtm'
2218 if 'approved' in lgtm_label:
2219 return 'lgtm'
2220
2221 if not data.get('reviewers', {}).get('REVIEWER', []):
2222 return 'unsent'
2223
2224 messages = data.get('messages', [])
2225 if messages:
2226 owner = data['owner'].get('_account_id')
2227 last_message_author = messages[-1].get('author', {}).get('_account_id')
2228 if owner != last_message_author:
2229 # Some reply from non-owner.
2230 return 'reply'
2231
2232 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002233
2234 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002235 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002236 return data['revisions'][data['current_revision']]['_number']
2237
2238 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002239 data = self._GetChangeDetail(['CURRENT_REVISION'])
2240 current_rev = data['current_revision']
2241 url = data['revisions'][current_rev]['fetch']['http']['url']
2242 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002243
2244 def UpdateDescriptionRemote(self, description):
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002245 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
2246 description)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002247
2248 def CloseIssue(self):
2249 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2250
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002251 def GetApprovingReviewers(self):
2252 """Returns a list of reviewers approving the change.
2253
2254 Note: not necessarily committers.
2255 """
2256 raise NotImplementedError()
2257
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002258 def SubmitIssue(self, wait_for_merge=True):
2259 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2260 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002261
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002262 def _GetChangeDetail(self, options=None, issue=None):
2263 options = options or []
2264 issue = issue or self.GetIssue()
2265 assert issue, 'issue required to query Gerrit'
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002266 return gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2267 options)
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002268
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002269 def CMDLand(self, force, bypass_hooks, verbose):
2270 if git_common.is_dirty_git_tree('land'):
2271 return 1
tandriid60367b2016-06-22 05:25:12 -07002272 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2273 if u'Commit-Queue' in detail.get('labels', {}):
2274 if not force:
2275 ask_for_data('\nIt seems this repository has a Commit Queue, '
2276 'which can test and land changes for you. '
2277 'Are you sure you wish to bypass it?\n'
2278 'Press Enter to continue, Ctrl+C to abort.')
2279
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002280 differs = True
2281 last_upload = RunGit(['config',
2282 'branch.%s.gerritsquashhash' % self.GetBranch()],
2283 error_ok=True).strip()
2284 # Note: git diff outputs nothing if there is no diff.
2285 if not last_upload or RunGit(['diff', last_upload]).strip():
2286 print('WARNING: some changes from local branch haven\'t been uploaded')
2287 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002288 if detail['current_revision'] == last_upload:
2289 differs = False
2290 else:
2291 print('WARNING: local branch contents differ from latest uploaded '
2292 'patchset')
2293 if differs:
2294 if not force:
2295 ask_for_data(
2296 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2297 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2298 elif not bypass_hooks:
2299 hook_results = self.RunHook(
2300 committing=True,
2301 may_prompt=not force,
2302 verbose=verbose,
2303 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2304 if not hook_results.should_continue():
2305 return 1
2306
2307 self.SubmitIssue(wait_for_merge=True)
2308 print('Issue %s has been submitted.' % self.GetIssueURL())
2309 return 0
2310
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002311 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2312 directory):
2313 assert not reject
2314 assert not nocommit
2315 assert not directory
2316 assert parsed_issue_arg.valid
2317
2318 self._changelist.issue = parsed_issue_arg.issue
2319
2320 if parsed_issue_arg.hostname:
2321 self._gerrit_host = parsed_issue_arg.hostname
2322 self._gerrit_server = 'https://%s' % self._gerrit_host
2323
2324 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2325
2326 if not parsed_issue_arg.patchset:
2327 # Use current revision by default.
2328 revision_info = detail['revisions'][detail['current_revision']]
2329 patchset = int(revision_info['_number'])
2330 else:
2331 patchset = parsed_issue_arg.patchset
2332 for revision_info in detail['revisions'].itervalues():
2333 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2334 break
2335 else:
2336 DieWithError('Couldn\'t find patchset %i in issue %i' %
2337 (parsed_issue_arg.patchset, self.GetIssue()))
2338
2339 fetch_info = revision_info['fetch']['http']
2340 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2341 RunGit(['cherry-pick', 'FETCH_HEAD'])
2342 self.SetIssue(self.GetIssue())
2343 self.SetPatchset(patchset)
2344 print('Committed patch for issue %i pathset %i locally' %
2345 (self.GetIssue(), self.GetPatchset()))
2346 return 0
2347
2348 @staticmethod
2349 def ParseIssueURL(parsed_url):
2350 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2351 return None
2352 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2353 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2354 # Short urls like https://domain/<issue_number> can be used, but don't allow
2355 # specifying the patchset (you'd 404), but we allow that here.
2356 if parsed_url.path == '/':
2357 part = parsed_url.fragment
2358 else:
2359 part = parsed_url.path
2360 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2361 if match:
2362 return _ParsedIssueNumberArgument(
2363 issue=int(match.group(2)),
2364 patchset=int(match.group(4)) if match.group(4) else None,
2365 hostname=parsed_url.netloc)
2366 return None
2367
tandrii16e0b4e2016-06-07 10:34:28 -07002368 def _GerritCommitMsgHookCheck(self, offer_removal):
2369 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2370 if not os.path.exists(hook):
2371 return
2372 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2373 # custom developer made one.
2374 data = gclient_utils.FileRead(hook)
2375 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2376 return
2377 print('Warning: you have Gerrit commit-msg hook installed.\n'
2378 'It is not neccessary for uploading with git cl in squash mode, '
2379 'and may interfere with it in subtle ways.\n'
2380 'We recommend you remove the commit-msg hook.')
2381 if offer_removal:
2382 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2383 if reply.lower().startswith('y'):
2384 gclient_utils.rm_file_or_tree(hook)
2385 print('Gerrit commit-msg hook removed.')
2386 else:
2387 print('OK, will keep Gerrit commit-msg hook in place.')
2388
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002389 def CMDUploadChange(self, options, args, change):
2390 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002391 if options.squash and options.no_squash:
2392 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002393
2394 if not options.squash and not options.no_squash:
2395 # Load default for user, repo, squash=true, in this order.
2396 options.squash = settings.GetSquashGerritUploads()
2397 elif options.no_squash:
2398 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002399
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002400 # We assume the remote called "origin" is the one we want.
2401 # It is probably not worthwhile to support different workflows.
2402 gerrit_remote = 'origin'
2403
2404 remote, remote_branch = self.GetRemoteBranch()
2405 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2406 pending_prefix='')
2407
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002408 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002409 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002410 if self.GetIssue():
2411 # Try to get the message from a previous upload.
2412 message = self.GetDescription()
2413 if not message:
2414 DieWithError(
2415 'failed to fetch description from current Gerrit issue %d\n'
2416 '%s' % (self.GetIssue(), self.GetIssueURL()))
2417 change_id = self._GetChangeDetail()['change_id']
2418 while True:
2419 footer_change_ids = git_footers.get_footer_change_id(message)
2420 if footer_change_ids == [change_id]:
2421 break
2422 if not footer_change_ids:
2423 message = git_footers.add_footer_change_id(message, change_id)
2424 print('WARNING: appended missing Change-Id to issue description')
2425 continue
2426 # There is already a valid footer but with different or several ids.
2427 # Doing this automatically is non-trivial as we don't want to lose
2428 # existing other footers, yet we want to append just 1 desired
2429 # Change-Id. Thus, just create a new footer, but let user verify the
2430 # new description.
2431 message = '%s\n\nChange-Id: %s' % (message, change_id)
2432 print(
2433 'WARNING: issue %s has Change-Id footer(s):\n'
2434 ' %s\n'
2435 'but issue has Change-Id %s, according to Gerrit.\n'
2436 'Please, check the proposed correction to the description, '
2437 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2438 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2439 change_id))
2440 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2441 if not options.force:
2442 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002443 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002444 message = change_desc.description
2445 if not message:
2446 DieWithError("Description is empty. Aborting...")
2447 # Continue the while loop.
2448 # Sanity check of this code - we should end up with proper message
2449 # footer.
2450 assert [change_id] == git_footers.get_footer_change_id(message)
2451 change_desc = ChangeDescription(message)
2452 else:
2453 change_desc = ChangeDescription(
2454 options.message or CreateDescriptionFromLog(args))
2455 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002456 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002457 if not change_desc.description:
2458 DieWithError("Description is empty. Aborting...")
2459 message = change_desc.description
2460 change_ids = git_footers.get_footer_change_id(message)
2461 if len(change_ids) > 1:
2462 DieWithError('too many Change-Id footers, at most 1 allowed.')
2463 if not change_ids:
2464 # Generate the Change-Id automatically.
2465 message = git_footers.add_footer_change_id(
2466 message, GenerateGerritChangeId(message))
2467 change_desc.set_description(message)
2468 change_ids = git_footers.get_footer_change_id(message)
2469 assert len(change_ids) == 1
2470 change_id = change_ids[0]
2471
2472 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2473 if remote is '.':
2474 # If our upstream branch is local, we base our squashed commit on its
2475 # squashed version.
2476 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2477 # Check the squashed hash of the parent.
2478 parent = RunGit(['config',
2479 'branch.%s.gerritsquashhash' % upstream_branch_name],
2480 error_ok=True).strip()
2481 # Verify that the upstream branch has been uploaded too, otherwise
2482 # Gerrit will create additional CLs when uploading.
2483 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2484 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002485 DieWithError(
2486 'Upload upstream branch %s first.\n'
tandrii2bdadf12016-07-12 12:27:54 -07002487 'Note: maybe you\'ve uploaded it with --no-squash. '
2488 'If so, then re-upload it with:\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002489 ' git cl upload --squash\n' % upstream_branch_name)
2490 else:
2491 parent = self.GetCommonAncestorWithUpstream()
2492
2493 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2494 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2495 '-m', message]).strip()
2496 else:
2497 change_desc = ChangeDescription(
2498 options.message or CreateDescriptionFromLog(args))
2499 if not change_desc.description:
2500 DieWithError("Description is empty. Aborting...")
2501
2502 if not git_footers.get_footer_change_id(change_desc.description):
2503 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002504 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2505 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002506 ref_to_push = 'HEAD'
2507 parent = '%s/%s' % (gerrit_remote, branch)
2508 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2509
2510 assert change_desc
2511 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2512 ref_to_push)]).splitlines()
2513 if len(commits) > 1:
2514 print('WARNING: This will upload %d commits. Run the following command '
2515 'to see which commits will be uploaded: ' % len(commits))
2516 print('git log %s..%s' % (parent, ref_to_push))
2517 print('You can also use `git squash-branch` to squash these into a '
2518 'single commit.')
2519 ask_for_data('About to upload; enter to confirm.')
2520
2521 if options.reviewers or options.tbr_owners:
2522 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2523 change)
2524
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002525 # Extra options that can be specified at push time. Doc:
2526 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2527 refspec_opts = []
2528 if options.title:
2529 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2530 # reverse on its side.
2531 if '_' in options.title:
2532 print('WARNING: underscores in title will be converted to spaces.')
2533 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2534
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002535 if options.send_mail:
2536 if not change_desc.get_reviewers():
2537 DieWithError('Must specify reviewers to send email.')
2538 refspec_opts.append('notify=ALL')
2539 else:
2540 refspec_opts.append('notify=NONE')
2541
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002542 cc = self.GetCCList().split(',')
2543 if options.cc:
2544 cc.extend(options.cc)
2545 cc = filter(None, cc)
2546 if cc:
tandrii@chromium.org074c2af2016-06-03 23:18:40 +00002547 refspec_opts.extend('cc=' + email.strip() for email in cc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002548
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002549 if change_desc.get_reviewers():
2550 refspec_opts.extend('r=' + email.strip()
2551 for email in change_desc.get_reviewers())
2552
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002553 refspec_suffix = ''
2554 if refspec_opts:
2555 refspec_suffix = '%' + ','.join(refspec_opts)
2556 assert ' ' not in refspec_suffix, (
2557 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002558 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002559
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002560 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002561 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002562 print_stdout=True,
2563 # Flush after every line: useful for seeing progress when running as
2564 # recipe.
2565 filter_fn=lambda _: sys.stdout.flush())
2566
2567 if options.squash:
2568 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2569 change_numbers = [m.group(1)
2570 for m in map(regex.match, push_stdout.splitlines())
2571 if m]
2572 if len(change_numbers) != 1:
2573 DieWithError(
2574 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2575 'Change-Id: %s') % (len(change_numbers), change_id))
2576 self.SetIssue(change_numbers[0])
2577 RunGit(['config', 'branch.%s.gerritsquashhash' % self.GetBranch(),
2578 ref_to_push])
2579 return 0
2580
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002581 def _AddChangeIdToCommitMessage(self, options, args):
2582 """Re-commits using the current message, assumes the commit hook is in
2583 place.
2584 """
2585 log_desc = options.message or CreateDescriptionFromLog(args)
2586 git_command = ['commit', '--amend', '-m', log_desc]
2587 RunGit(git_command)
2588 new_log_desc = CreateDescriptionFromLog(args)
2589 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002590 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002591 return new_log_desc
2592 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002593 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002594
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002595 def SetCQState(self, new_state):
2596 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002597 vote_map = {
2598 _CQState.NONE: 0,
2599 _CQState.DRY_RUN: 1,
2600 _CQState.COMMIT : 2,
2601 }
2602 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2603 labels={'Commit-Queue': vote_map[new_state]})
2604
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002605
2606_CODEREVIEW_IMPLEMENTATIONS = {
2607 'rietveld': _RietveldChangelistImpl,
2608 'gerrit': _GerritChangelistImpl,
2609}
2610
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002611
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002612def _add_codereview_select_options(parser):
2613 """Appends --gerrit and --rietveld options to force specific codereview."""
2614 parser.codereview_group = optparse.OptionGroup(
2615 parser, 'EXPERIMENTAL! Codereview override options')
2616 parser.add_option_group(parser.codereview_group)
2617 parser.codereview_group.add_option(
2618 '--gerrit', action='store_true',
2619 help='Force the use of Gerrit for codereview')
2620 parser.codereview_group.add_option(
2621 '--rietveld', action='store_true',
2622 help='Force the use of Rietveld for codereview')
2623
2624
2625def _process_codereview_select_options(parser, options):
2626 if options.gerrit and options.rietveld:
2627 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2628 options.forced_codereview = None
2629 if options.gerrit:
2630 options.forced_codereview = 'gerrit'
2631 elif options.rietveld:
2632 options.forced_codereview = 'rietveld'
2633
2634
tandriif9aefb72016-07-01 09:06:51 -07002635def _get_bug_line_values(default_project, bugs):
2636 """Given default_project and comma separated list of bugs, yields bug line
2637 values.
2638
2639 Each bug can be either:
2640 * a number, which is combined with default_project
2641 * string, which is left as is.
2642
2643 This function may produce more than one line, because bugdroid expects one
2644 project per line.
2645
2646 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2647 ['v8:123', 'chromium:789']
2648 """
2649 default_bugs = []
2650 others = []
2651 for bug in bugs.split(','):
2652 bug = bug.strip()
2653 if bug:
2654 try:
2655 default_bugs.append(int(bug))
2656 except ValueError:
2657 others.append(bug)
2658
2659 if default_bugs:
2660 default_bugs = ','.join(map(str, default_bugs))
2661 if default_project:
2662 yield '%s:%s' % (default_project, default_bugs)
2663 else:
2664 yield default_bugs
2665 for other in sorted(others):
2666 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2667 yield other
2668
2669
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002670class ChangeDescription(object):
2671 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002672 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002673 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002674
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002675 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002676 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002677
agable@chromium.org42c20792013-09-12 17:34:49 +00002678 @property # www.logilab.org/ticket/89786
2679 def description(self): # pylint: disable=E0202
2680 return '\n'.join(self._description_lines)
2681
2682 def set_description(self, desc):
2683 if isinstance(desc, basestring):
2684 lines = desc.splitlines()
2685 else:
2686 lines = [line.rstrip() for line in desc]
2687 while lines and not lines[0]:
2688 lines.pop(0)
2689 while lines and not lines[-1]:
2690 lines.pop(-1)
2691 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002692
piman@chromium.org336f9122014-09-04 02:16:55 +00002693 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002694 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002695 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002696 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002697 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002698 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002699
agable@chromium.org42c20792013-09-12 17:34:49 +00002700 # Get the set of R= and TBR= lines and remove them from the desciption.
2701 regexp = re.compile(self.R_LINE)
2702 matches = [regexp.match(line) for line in self._description_lines]
2703 new_desc = [l for i, l in enumerate(self._description_lines)
2704 if not matches[i]]
2705 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002706
agable@chromium.org42c20792013-09-12 17:34:49 +00002707 # Construct new unified R= and TBR= lines.
2708 r_names = []
2709 tbr_names = []
2710 for match in matches:
2711 if not match:
2712 continue
2713 people = cleanup_list([match.group(2).strip()])
2714 if match.group(1) == 'TBR':
2715 tbr_names.extend(people)
2716 else:
2717 r_names.extend(people)
2718 for name in r_names:
2719 if name not in reviewers:
2720 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002721 if add_owners_tbr:
2722 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07002723 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00002724 all_reviewers = set(tbr_names + reviewers)
2725 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2726 all_reviewers)
2727 tbr_names.extend(owners_db.reviewers_for(missing_files,
2728 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002729 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2730 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2731
2732 # Put the new lines in the description where the old first R= line was.
2733 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2734 if 0 <= line_loc < len(self._description_lines):
2735 if new_tbr_line:
2736 self._description_lines.insert(line_loc, new_tbr_line)
2737 if new_r_line:
2738 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002739 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002740 if new_r_line:
2741 self.append_footer(new_r_line)
2742 if new_tbr_line:
2743 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002744
tandriif9aefb72016-07-01 09:06:51 -07002745 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002746 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002747 self.set_description([
2748 '# Enter a description of the change.',
2749 '# This will be displayed on the codereview site.',
2750 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002751 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002752 '--------------------',
2753 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002754
agable@chromium.org42c20792013-09-12 17:34:49 +00002755 regexp = re.compile(self.BUG_LINE)
2756 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07002757 prefix = settings.GetBugPrefix()
2758 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
2759 for value in values:
2760 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
2761 self.append_footer('BUG=%s' % value)
2762
agable@chromium.org42c20792013-09-12 17:34:49 +00002763 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002764 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002765 if not content:
2766 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002767 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002768
2769 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002770 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2771 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002772 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002773 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002774
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002775 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00002776 """Adds a footer line to the description.
2777
2778 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
2779 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
2780 that Gerrit footers are always at the end.
2781 """
2782 parsed_footer_line = git_footers.parse_footer(line)
2783 if parsed_footer_line:
2784 # Line is a gerrit footer in the form: Footer-Key: any value.
2785 # Thus, must be appended observing Gerrit footer rules.
2786 self.set_description(
2787 git_footers.add_footer(self.description,
2788 key=parsed_footer_line[0],
2789 value=parsed_footer_line[1]))
2790 return
2791
2792 if not self._description_lines:
2793 self._description_lines.append(line)
2794 return
2795
2796 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
2797 if gerrit_footers:
2798 # git_footers.split_footers ensures that there is an empty line before
2799 # actual (gerrit) footers, if any. We have to keep it that way.
2800 assert top_lines and top_lines[-1] == ''
2801 top_lines, separator = top_lines[:-1], top_lines[-1:]
2802 else:
2803 separator = [] # No need for separator if there are no gerrit_footers.
2804
2805 prev_line = top_lines[-1] if top_lines else ''
2806 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
2807 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2808 top_lines.append('')
2809 top_lines.append(line)
2810 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002811
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002812 def get_reviewers(self):
2813 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002814 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
2815 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002816 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002817
2818
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002819def get_approving_reviewers(props):
2820 """Retrieves the reviewers that approved a CL from the issue properties with
2821 messages.
2822
2823 Note that the list may contain reviewers that are not committer, thus are not
2824 considered by the CQ.
2825 """
2826 return sorted(
2827 set(
2828 message['sender']
2829 for message in props['messages']
2830 if message['approval'] and message['sender'] in props['reviewers']
2831 )
2832 )
2833
2834
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002835def FindCodereviewSettingsFile(filename='codereview.settings'):
2836 """Finds the given file starting in the cwd and going up.
2837
2838 Only looks up to the top of the repository unless an
2839 'inherit-review-settings-ok' file exists in the root of the repository.
2840 """
2841 inherit_ok_file = 'inherit-review-settings-ok'
2842 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002843 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002844 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2845 root = '/'
2846 while True:
2847 if filename in os.listdir(cwd):
2848 if os.path.isfile(os.path.join(cwd, filename)):
2849 return open(os.path.join(cwd, filename))
2850 if cwd == root:
2851 break
2852 cwd = os.path.dirname(cwd)
2853
2854
2855def LoadCodereviewSettingsFromFile(fileobj):
2856 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002857 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002858
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002859 def SetProperty(name, setting, unset_error_ok=False):
2860 fullname = 'rietveld.' + name
2861 if setting in keyvals:
2862 RunGit(['config', fullname, keyvals[setting]])
2863 else:
2864 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2865
2866 SetProperty('server', 'CODE_REVIEW_SERVER')
2867 # Only server setting is required. Other settings can be absent.
2868 # In that case, we ignore errors raised during option deletion attempt.
2869 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002870 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002871 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2872 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002873 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002874 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002875 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2876 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002877 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002878 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002879 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002880 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2881 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002882
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002883 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002884 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002885
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002886 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07002887 RunGit(['config', 'gerrit.squash-uploads',
2888 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002889
tandrii@chromium.org28253532016-04-14 13:46:56 +00002890 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00002891 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00002892 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
2893
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002894 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2895 #should be of the form
2896 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2897 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2898 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2899 keyvals['ORIGIN_URL_CONFIG']])
2900
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002901
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002902def urlretrieve(source, destination):
2903 """urllib is broken for SSL connections via a proxy therefore we
2904 can't use urllib.urlretrieve()."""
2905 with open(destination, 'w') as f:
2906 f.write(urllib2.urlopen(source).read())
2907
2908
ukai@chromium.org712d6102013-11-27 00:52:58 +00002909def hasSheBang(fname):
2910 """Checks fname is a #! script."""
2911 with open(fname) as f:
2912 return f.read(2).startswith('#!')
2913
2914
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00002915# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
2916def DownloadHooks(*args, **kwargs):
2917 pass
2918
2919
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002920def DownloadGerritHook(force):
2921 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002922
2923 Args:
2924 force: True to update hooks. False to install hooks if not present.
2925 """
2926 if not settings.GetIsGerrit():
2927 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00002928 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002929 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2930 if not os.access(dst, os.X_OK):
2931 if os.path.exists(dst):
2932 if not force:
2933 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002934 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002935 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002936 if not hasSheBang(dst):
2937 DieWithError('Not a script: %s\n'
2938 'You need to download from\n%s\n'
2939 'into .git/hooks/commit-msg and '
2940 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002941 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2942 except Exception:
2943 if os.path.exists(dst):
2944 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002945 DieWithError('\nFailed to download hooks.\n'
2946 'You need to download from\n%s\n'
2947 'into .git/hooks/commit-msg and '
2948 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002949
2950
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002951
2952def GetRietveldCodereviewSettingsInteractively():
2953 """Prompt the user for settings."""
2954 server = settings.GetDefaultServerUrl(error_ok=True)
2955 prompt = 'Rietveld server (host[:port])'
2956 prompt += ' [%s]' % (server or DEFAULT_SERVER)
2957 newserver = ask_for_data(prompt + ':')
2958 if not server and not newserver:
2959 newserver = DEFAULT_SERVER
2960 if newserver:
2961 newserver = gclient_utils.UpgradeToHttps(newserver)
2962 if newserver != server:
2963 RunGit(['config', 'rietveld.server', newserver])
2964
2965 def SetProperty(initial, caption, name, is_url):
2966 prompt = caption
2967 if initial:
2968 prompt += ' ("x" to clear) [%s]' % initial
2969 new_val = ask_for_data(prompt + ':')
2970 if new_val == 'x':
2971 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
2972 elif new_val:
2973 if is_url:
2974 new_val = gclient_utils.UpgradeToHttps(new_val)
2975 if new_val != initial:
2976 RunGit(['config', 'rietveld.' + name, new_val])
2977
2978 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
2979 SetProperty(settings.GetDefaultPrivateFlag(),
2980 'Private flag (rietveld only)', 'private', False)
2981 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
2982 'tree-status-url', False)
2983 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
2984 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
2985 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
2986 'run-post-upload-hook', False)
2987
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002988@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002989def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002990 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002991
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002992 print('WARNING: git cl config works for Rietveld only.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002993 'For Gerrit, see http://crbug.com/603116.')
2994 # TODO(tandrii): add Gerrit support as part of http://crbug.com/603116.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00002995 parser.add_option('--activate-update', action='store_true',
2996 help='activate auto-updating [rietveld] section in '
2997 '.git/config')
2998 parser.add_option('--deactivate-update', action='store_true',
2999 help='deactivate auto-updating [rietveld] section in '
3000 '.git/config')
3001 options, args = parser.parse_args(args)
3002
3003 if options.deactivate_update:
3004 RunGit(['config', 'rietveld.autoupdate', 'false'])
3005 return
3006
3007 if options.activate_update:
3008 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3009 return
3010
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003011 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003012 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003013 return 0
3014
3015 url = args[0]
3016 if not url.endswith('codereview.settings'):
3017 url = os.path.join(url, 'codereview.settings')
3018
3019 # Load code review settings and download hooks (if available).
3020 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3021 return 0
3022
3023
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003024def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003025 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003026 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3027 branch = ShortBranchName(branchref)
3028 _, args = parser.parse_args(args)
3029 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003030 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003031 return RunGit(['config', 'branch.%s.base-url' % branch],
3032 error_ok=False).strip()
3033 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003034 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003035 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3036 error_ok=False).strip()
3037
3038
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003039def color_for_status(status):
3040 """Maps a Changelist status to color, for CMDstatus and other tools."""
3041 return {
3042 'unsent': Fore.RED,
3043 'waiting': Fore.BLUE,
3044 'reply': Fore.YELLOW,
3045 'lgtm': Fore.GREEN,
3046 'commit': Fore.MAGENTA,
3047 'closed': Fore.CYAN,
3048 'error': Fore.WHITE,
3049 }.get(status, Fore.WHITE)
3050
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003051
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003052def get_cl_statuses(changes, fine_grained, max_processes=None):
3053 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003054
3055 If fine_grained is true, this will fetch CL statuses from the server.
3056 Otherwise, simply indicate if there's a matching url for the given branches.
3057
3058 If max_processes is specified, it is used as the maximum number of processes
3059 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3060 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003061
3062 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003063 """
3064 # Silence upload.py otherwise it becomes unwieldly.
3065 upload.verbosity = 0
3066
3067 if fine_grained:
3068 # Process one branch synchronously to work through authentication, then
3069 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003070 if changes:
3071 fetch = lambda cl: (cl, cl.GetStatus())
3072 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003073
kmarshall3bff56b2016-06-06 18:31:47 -07003074 if not changes:
3075 # Exit early if there was only one branch to fetch.
3076 return
3077
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003078 changes_to_fetch = changes[1:]
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003079 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003080 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003081 if max_processes is not None
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003082 else len(changes_to_fetch))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003083
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003084 fetched_cls = set()
3085 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003086 while True:
3087 try:
3088 row = it.next(timeout=5)
3089 except multiprocessing.TimeoutError:
3090 break
3091
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003092 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003093 yield row
3094
3095 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003096 for cl in set(changes_to_fetch) - fetched_cls:
3097 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003098
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003099 else:
3100 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003101 for cl in changes:
3102 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003103
rmistry@google.com2dd99862015-06-22 12:22:18 +00003104
3105def upload_branch_deps(cl, args):
3106 """Uploads CLs of local branches that are dependents of the current branch.
3107
3108 If the local branch dependency tree looks like:
3109 test1 -> test2.1 -> test3.1
3110 -> test3.2
3111 -> test2.2 -> test3.3
3112
3113 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3114 run on the dependent branches in this order:
3115 test2.1, test3.1, test3.2, test2.2, test3.3
3116
3117 Note: This function does not rebase your local dependent branches. Use it when
3118 you make a change to the parent branch that will not conflict with its
3119 dependent branches, and you would like their dependencies updated in
3120 Rietveld.
3121 """
3122 if git_common.is_dirty_git_tree('upload-branch-deps'):
3123 return 1
3124
3125 root_branch = cl.GetBranch()
3126 if root_branch is None:
3127 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3128 'Get on a branch!')
3129 if not cl.GetIssue() or not cl.GetPatchset():
3130 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3131 'patchset dependencies without an uploaded CL.')
3132
3133 branches = RunGit(['for-each-ref',
3134 '--format=%(refname:short) %(upstream:short)',
3135 'refs/heads'])
3136 if not branches:
3137 print('No local branches found.')
3138 return 0
3139
3140 # Create a dictionary of all local branches to the branches that are dependent
3141 # on it.
3142 tracked_to_dependents = collections.defaultdict(list)
3143 for b in branches.splitlines():
3144 tokens = b.split()
3145 if len(tokens) == 2:
3146 branch_name, tracked = tokens
3147 tracked_to_dependents[tracked].append(branch_name)
3148
vapiera7fbd5a2016-06-16 09:17:49 -07003149 print()
3150 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003151 dependents = []
3152 def traverse_dependents_preorder(branch, padding=''):
3153 dependents_to_process = tracked_to_dependents.get(branch, [])
3154 padding += ' '
3155 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003156 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003157 dependents.append(dependent)
3158 traverse_dependents_preorder(dependent, padding)
3159 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003160 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003161
3162 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003163 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003164 return 0
3165
vapiera7fbd5a2016-06-16 09:17:49 -07003166 print('This command will checkout all dependent branches and run '
3167 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003168 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3169
andybons@chromium.org962f9462016-02-03 20:00:42 +00003170 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003171 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003172 args.extend(['-t', 'Updated patchset dependency'])
3173
rmistry@google.com2dd99862015-06-22 12:22:18 +00003174 # Record all dependents that failed to upload.
3175 failures = {}
3176 # Go through all dependents, checkout the branch and upload.
3177 try:
3178 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003179 print()
3180 print('--------------------------------------')
3181 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003182 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003183 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003184 try:
3185 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003186 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003187 failures[dependent_branch] = 1
3188 except: # pylint: disable=W0702
3189 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003190 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003191 finally:
3192 # Swap back to the original root branch.
3193 RunGit(['checkout', '-q', root_branch])
3194
vapiera7fbd5a2016-06-16 09:17:49 -07003195 print()
3196 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003197 for dependent_branch in dependents:
3198 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003199 print(' %s : %s' % (dependent_branch, upload_status))
3200 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003201
3202 return 0
3203
3204
kmarshall3bff56b2016-06-06 18:31:47 -07003205def CMDarchive(parser, args):
3206 """Archives and deletes branches associated with closed changelists."""
3207 parser.add_option(
3208 '-j', '--maxjobs', action='store', type=int,
3209 help='The maximum number of jobs to use when retrieving review status')
3210 parser.add_option(
3211 '-f', '--force', action='store_true',
3212 help='Bypasses the confirmation prompt.')
3213
3214 auth.add_auth_options(parser)
3215 options, args = parser.parse_args(args)
3216 if args:
3217 parser.error('Unsupported args: %s' % ' '.join(args))
3218 auth_config = auth.extract_auth_config_from_options(options)
3219
3220 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3221 if not branches:
3222 return 0
3223
vapiera7fbd5a2016-06-16 09:17:49 -07003224 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003225 changes = [Changelist(branchref=b, auth_config=auth_config)
3226 for b in branches.splitlines()]
3227 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3228 statuses = get_cl_statuses(changes,
3229 fine_grained=True,
3230 max_processes=options.maxjobs)
3231 proposal = [(cl.GetBranch(),
3232 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3233 for cl, status in statuses
3234 if status == 'closed']
3235 proposal.sort()
3236
3237 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003238 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003239 return 0
3240
3241 current_branch = GetCurrentBranch()
3242
vapiera7fbd5a2016-06-16 09:17:49 -07003243 print('\nBranches with closed issues that will be archived:\n')
3244 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
kmarshall3bff56b2016-06-06 18:31:47 -07003245 for next_item in proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003246 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003247
3248 if any(branch == current_branch for branch, _ in proposal):
3249 print('You are currently on a branch \'%s\' which is associated with a '
3250 'closed codereview issue, so archive cannot proceed. Please '
3251 'checkout another branch and run this command again.' %
3252 current_branch)
3253 return 1
3254
3255 if not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003256 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3257 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003258 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003259 return 1
3260
3261 for branch, tagname in proposal:
3262 RunGit(['tag', tagname, branch])
3263 RunGit(['branch', '-D', branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003264 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003265
3266 return 0
3267
3268
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003269def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003270 """Show status of changelists.
3271
3272 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003273 - Red not sent for review or broken
3274 - Blue waiting for review
3275 - Yellow waiting for you to reply to review
3276 - Green LGTM'ed
3277 - Magenta in the commit queue
3278 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003279
3280 Also see 'git cl comments'.
3281 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003282 parser.add_option('--field',
3283 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003284 parser.add_option('-f', '--fast', action='store_true',
3285 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003286 parser.add_option(
3287 '-j', '--maxjobs', action='store', type=int,
3288 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003289
3290 auth.add_auth_options(parser)
3291 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003292 if args:
3293 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003294 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003295
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003296 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003297 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003298 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003299 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003300 elif options.field == 'id':
3301 issueid = cl.GetIssue()
3302 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003303 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003304 elif options.field == 'patch':
3305 patchset = cl.GetPatchset()
3306 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003307 print(patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003308 elif options.field == 'url':
3309 url = cl.GetIssueURL()
3310 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003311 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003312 return 0
3313
3314 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3315 if not branches:
3316 print('No local branch found.')
3317 return 0
3318
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003319 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003320 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003321 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003322 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003323 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003324 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003325 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003326
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003327 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003328 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3329 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3330 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003331 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003332 c, status = output.next()
3333 branch_statuses[c.GetBranch()] = status
3334 status = branch_statuses.pop(branch)
3335 url = cl.GetIssueURL()
3336 if url and (not status or status == 'error'):
3337 # The issue probably doesn't exist anymore.
3338 url += ' (broken)'
3339
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003340 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003341 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003342 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003343 color = ''
3344 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003345 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003346 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003347 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003348 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003349
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003350 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003351 print()
3352 print('Current branch:',)
3353 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003354 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003355 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003356 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003357 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003358 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003359 print('Issue description:')
3360 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003361 return 0
3362
3363
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003364def colorize_CMDstatus_doc():
3365 """To be called once in main() to add colors to git cl status help."""
3366 colors = [i for i in dir(Fore) if i[0].isupper()]
3367
3368 def colorize_line(line):
3369 for color in colors:
3370 if color in line.upper():
3371 # Extract whitespaces first and the leading '-'.
3372 indent = len(line) - len(line.lstrip(' ')) + 1
3373 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3374 return line
3375
3376 lines = CMDstatus.__doc__.splitlines()
3377 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3378
3379
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003380@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003381def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003382 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003383
3384 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003385 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003386 parser.add_option('-r', '--reverse', action='store_true',
3387 help='Lookup the branch(es) for the specified issues. If '
3388 'no issues are specified, all branches with mapped '
3389 'issues will be listed.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003390 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003391 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003392 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003393
dnj@chromium.org406c4402015-03-03 17:22:28 +00003394 if options.reverse:
3395 branches = RunGit(['for-each-ref', 'refs/heads',
3396 '--format=%(refname:short)']).splitlines()
3397
3398 # Reverse issue lookup.
3399 issue_branch_map = {}
3400 for branch in branches:
3401 cl = Changelist(branchref=branch)
3402 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3403 if not args:
3404 args = sorted(issue_branch_map.iterkeys())
3405 for issue in args:
3406 if not issue:
3407 continue
vapiera7fbd5a2016-06-16 09:17:49 -07003408 print('Branch for issue number %s: %s' % (
3409 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
dnj@chromium.org406c4402015-03-03 17:22:28 +00003410 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003411 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003412 if len(args) > 0:
3413 try:
3414 issue = int(args[0])
3415 except ValueError:
3416 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003417 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003418 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003419 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003420 return 0
3421
3422
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003423def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003424 """Shows or posts review comments for any changelist."""
3425 parser.add_option('-a', '--add-comment', dest='comment',
3426 help='comment to add to an issue')
3427 parser.add_option('-i', dest='issue',
3428 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003429 parser.add_option('-j', '--json-file',
3430 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003431 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003432 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003433 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003434
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003435 issue = None
3436 if options.issue:
3437 try:
3438 issue = int(options.issue)
3439 except ValueError:
3440 DieWithError('A review issue id is expected to be a number')
3441
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003442 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003443
3444 if options.comment:
3445 cl.AddComment(options.comment)
3446 return 0
3447
3448 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003449 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003450 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003451 summary.append({
3452 'date': message['date'],
3453 'lgtm': False,
3454 'message': message['text'],
3455 'not_lgtm': False,
3456 'sender': message['sender'],
3457 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003458 if message['disapproval']:
3459 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003460 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003461 elif message['approval']:
3462 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003463 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003464 elif message['sender'] == data['owner_email']:
3465 color = Fore.MAGENTA
3466 else:
3467 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003468 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003469 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003470 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003471 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003472 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003473 if options.json_file:
3474 with open(options.json_file, 'wb') as f:
3475 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003476 return 0
3477
3478
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003479@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003480def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003481 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003482 parser.add_option('-d', '--display', action='store_true',
3483 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003484 parser.add_option('-n', '--new-description',
3485 help='New description to set for this issue (- for stdin)')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003486
3487 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003488 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003489 options, args = parser.parse_args(args)
3490 _process_codereview_select_options(parser, options)
3491
3492 target_issue = None
3493 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07003494 target_issue = ParseIssueNumberArgument(args[0])
3495 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003496 parser.print_help()
3497 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003498
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003499 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003500
martiniss6eda05f2016-06-30 10:18:35 -07003501 kwargs = {
3502 'auth_config': auth_config,
3503 'codereview': options.forced_codereview,
3504 }
3505 if target_issue:
3506 kwargs['issue'] = target_issue.issue
3507 if options.forced_codereview == 'rietveld':
3508 kwargs['rietveld_server'] = target_issue.hostname
3509
3510 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003511
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003512 if not cl.GetIssue():
3513 DieWithError('This branch has no associated changelist.')
3514 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003515
smut@google.com34fb6b12015-07-13 20:03:26 +00003516 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003517 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003518 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003519
3520 if options.new_description:
3521 text = options.new_description
3522 if text == '-':
3523 text = '\n'.join(l.rstrip() for l in sys.stdin)
3524
3525 description.set_description(text)
3526 else:
3527 description.prompt()
3528
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003529 if cl.GetDescription() != description.description:
3530 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003531 return 0
3532
3533
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003534def CreateDescriptionFromLog(args):
3535 """Pulls out the commit log to use as a base for the CL description."""
3536 log_args = []
3537 if len(args) == 1 and not args[0].endswith('.'):
3538 log_args = [args[0] + '..']
3539 elif len(args) == 1 and args[0].endswith('...'):
3540 log_args = [args[0][:-1]]
3541 elif len(args) == 2:
3542 log_args = [args[0] + '..' + args[1]]
3543 else:
3544 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003545 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003546
3547
thestig@chromium.org44202a22014-03-11 19:22:18 +00003548def CMDlint(parser, args):
3549 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003550 parser.add_option('--filter', action='append', metavar='-x,+y',
3551 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003552 auth.add_auth_options(parser)
3553 options, args = parser.parse_args(args)
3554 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003555
3556 # Access to a protected member _XX of a client class
3557 # pylint: disable=W0212
3558 try:
3559 import cpplint
3560 import cpplint_chromium
3561 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003562 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003563 return 1
3564
3565 # Change the current working directory before calling lint so that it
3566 # shows the correct base.
3567 previous_cwd = os.getcwd()
3568 os.chdir(settings.GetRoot())
3569 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003570 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003571 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3572 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003573 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003574 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003575 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003576
3577 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003578 command = args + files
3579 if options.filter:
3580 command = ['--filter=' + ','.join(options.filter)] + command
3581 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003582
3583 white_regex = re.compile(settings.GetLintRegex())
3584 black_regex = re.compile(settings.GetLintIgnoreRegex())
3585 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3586 for filename in filenames:
3587 if white_regex.match(filename):
3588 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07003589 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003590 else:
3591 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3592 extra_check_functions)
3593 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003594 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003595 finally:
3596 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07003597 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003598 if cpplint._cpplint_state.error_count != 0:
3599 return 1
3600 return 0
3601
3602
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003603def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003604 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003605 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003606 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003607 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003608 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003609 auth.add_auth_options(parser)
3610 options, args = parser.parse_args(args)
3611 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003612
sbc@chromium.org71437c02015-04-09 19:29:40 +00003613 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07003614 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003615 return 1
3616
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003617 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003618 if args:
3619 base_branch = args[0]
3620 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003621 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003622 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003623
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003624 cl.RunHook(
3625 committing=not options.upload,
3626 may_prompt=False,
3627 verbose=options.verbose,
3628 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003629 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003630
3631
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003632def GenerateGerritChangeId(message):
3633 """Returns Ixxxxxx...xxx change id.
3634
3635 Works the same way as
3636 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3637 but can be called on demand on all platforms.
3638
3639 The basic idea is to generate git hash of a state of the tree, original commit
3640 message, author/committer info and timestamps.
3641 """
3642 lines = []
3643 tree_hash = RunGitSilent(['write-tree'])
3644 lines.append('tree %s' % tree_hash.strip())
3645 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3646 if code == 0:
3647 lines.append('parent %s' % parent.strip())
3648 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3649 lines.append('author %s' % author.strip())
3650 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3651 lines.append('committer %s' % committer.strip())
3652 lines.append('')
3653 # Note: Gerrit's commit-hook actually cleans message of some lines and
3654 # whitespace. This code is not doing this, but it clearly won't decrease
3655 # entropy.
3656 lines.append(message)
3657 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3658 stdin='\n'.join(lines))
3659 return 'I%s' % change_hash.strip()
3660
3661
wittman@chromium.org455dc922015-01-26 20:15:50 +00003662def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3663 """Computes the remote branch ref to use for the CL.
3664
3665 Args:
3666 remote (str): The git remote for the CL.
3667 remote_branch (str): The git remote branch for the CL.
3668 target_branch (str): The target branch specified by the user.
3669 pending_prefix (str): The pending prefix from the settings.
3670 """
3671 if not (remote and remote_branch):
3672 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003673
wittman@chromium.org455dc922015-01-26 20:15:50 +00003674 if target_branch:
3675 # Cannonicalize branch references to the equivalent local full symbolic
3676 # refs, which are then translated into the remote full symbolic refs
3677 # below.
3678 if '/' not in target_branch:
3679 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3680 else:
3681 prefix_replacements = (
3682 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3683 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3684 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3685 )
3686 match = None
3687 for regex, replacement in prefix_replacements:
3688 match = re.search(regex, target_branch)
3689 if match:
3690 remote_branch = target_branch.replace(match.group(0), replacement)
3691 break
3692 if not match:
3693 # This is a branch path but not one we recognize; use as-is.
3694 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003695 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3696 # Handle the refs that need to land in different refs.
3697 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003698
wittman@chromium.org455dc922015-01-26 20:15:50 +00003699 # Create the true path to the remote branch.
3700 # Does the following translation:
3701 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3702 # * refs/remotes/origin/master -> refs/heads/master
3703 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3704 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3705 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3706 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3707 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3708 'refs/heads/')
3709 elif remote_branch.startswith('refs/remotes/branch-heads'):
3710 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3711 # If a pending prefix exists then replace refs/ with it.
3712 if pending_prefix:
3713 remote_branch = remote_branch.replace('refs/', pending_prefix)
3714 return remote_branch
3715
3716
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003717def cleanup_list(l):
3718 """Fixes a list so that comma separated items are put as individual items.
3719
3720 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3721 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3722 """
3723 items = sum((i.split(',') for i in l), [])
3724 stripped_items = (i.strip() for i in items)
3725 return sorted(filter(None, stripped_items))
3726
3727
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003728@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003729def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003730 """Uploads the current changelist to codereview.
3731
3732 Can skip dependency patchset uploads for a branch by running:
3733 git config branch.branch_name.skip-deps-uploads True
3734 To unset run:
3735 git config --unset branch.branch_name.skip-deps-uploads
3736 Can also set the above globally by using the --global flag.
3737 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003738 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3739 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003740 parser.add_option('--bypass-watchlists', action='store_true',
3741 dest='bypass_watchlists',
3742 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003743 parser.add_option('-f', action='store_true', dest='force',
3744 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003745 parser.add_option('-m', dest='message', help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07003746 parser.add_option('-b', '--bug',
3747 help='pre-populate the bug number(s) for this issue. '
3748 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07003749 parser.add_option('--message-file', dest='message_file',
3750 help='file which contains message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003751 parser.add_option('-t', dest='title',
3752 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003753 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003754 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003755 help='reviewer email addresses')
3756 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003757 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003758 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003759 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003760 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003761 parser.add_option('--emulate_svn_auto_props',
3762 '--emulate-svn-auto-props',
3763 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003764 dest="emulate_svn_auto_props",
3765 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003766 parser.add_option('-c', '--use-commit-queue', action='store_true',
3767 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003768 parser.add_option('--private', action='store_true',
3769 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003770 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003771 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003772 metavar='TARGET',
3773 help='Apply CL to remote ref TARGET. ' +
3774 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003775 parser.add_option('--squash', action='store_true',
3776 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003777 parser.add_option('--no-squash', action='store_true',
3778 help='Don\'t squash multiple commits into one ' +
3779 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003780 parser.add_option('--email', default=None,
3781 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003782 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3783 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003784 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3785 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003786 help='Send the patchset to do a CQ dry run right after '
3787 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003788 parser.add_option('--dependencies', action='store_true',
3789 help='Uploads CLs of all the local branches that depend on '
3790 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003791
rmistry@google.com2dd99862015-06-22 12:22:18 +00003792 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003793 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003794 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003795 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003796 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003797 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003798 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003799
sbc@chromium.org71437c02015-04-09 19:29:40 +00003800 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003801 return 1
3802
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003803 options.reviewers = cleanup_list(options.reviewers)
3804 options.cc = cleanup_list(options.cc)
3805
tandriib80458a2016-06-23 12:20:07 -07003806 if options.message_file:
3807 if options.message:
3808 parser.error('only one of --message and --message-file allowed.')
3809 options.message = gclient_utils.FileRead(options.message_file)
3810 options.message_file = None
3811
tandrii4d0545a2016-07-06 03:56:49 -07003812 if options.cq_dry_run and options.use_commit_queue:
3813 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
3814
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003815 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3816 settings.GetIsGerrit()
3817
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003818 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00003819 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003820
3821
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003822def IsSubmoduleMergeCommit(ref):
3823 # When submodules are added to the repo, we expect there to be a single
3824 # non-git-svn merge commit at remote HEAD with a signature comment.
3825 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003826 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003827 return RunGit(cmd) != ''
3828
3829
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003830def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003831 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003832
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003833 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3834 upstream and closes the issue automatically and atomically.
3835
3836 Otherwise (in case of Rietveld):
3837 Squashes branch into a single commit.
3838 Updates changelog with metadata (e.g. pointer to review).
3839 Pushes/dcommits the code upstream.
3840 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003841 """
3842 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3843 help='bypass upload presubmit hook')
3844 parser.add_option('-m', dest='message',
3845 help="override review description")
3846 parser.add_option('-f', action='store_true', dest='force',
3847 help="force yes to questions (don't prompt)")
3848 parser.add_option('-c', dest='contributor',
3849 help="external contributor for patch (appended to " +
3850 "description and used as author for git). Should be " +
3851 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003852 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003853 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003854 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003855 auth_config = auth.extract_auth_config_from_options(options)
3856
3857 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003858
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003859 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3860 if cl.IsGerrit():
3861 if options.message:
3862 # This could be implemented, but it requires sending a new patch to
3863 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3864 # Besides, Gerrit has the ability to change the commit message on submit
3865 # automatically, thus there is no need to support this option (so far?).
3866 parser.error('-m MESSAGE option is not supported for Gerrit.')
3867 if options.contributor:
3868 parser.error(
3869 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3870 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3871 'the contributor\'s "name <email>". If you can\'t upload such a '
3872 'commit for review, contact your repository admin and request'
3873 '"Forge-Author" permission.')
3874 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3875 options.verbose)
3876
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003877 current = cl.GetBranch()
3878 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3879 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07003880 print()
3881 print('Attempting to push branch %r into another local branch!' % current)
3882 print()
3883 print('Either reparent this branch on top of origin/master:')
3884 print(' git reparent-branch --root')
3885 print()
3886 print('OR run `git rebase-update` if you think the parent branch is ')
3887 print('already committed.')
3888 print()
3889 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003890 return 1
3891
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003892 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003893 # Default to merging against our best guess of the upstream branch.
3894 args = [cl.GetUpstreamBranch()]
3895
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003896 if options.contributor:
3897 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07003898 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003899 return 1
3900
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003901 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003902 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003903
sbc@chromium.org71437c02015-04-09 19:29:40 +00003904 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003905 return 1
3906
3907 # This rev-list syntax means "show all commits not in my branch that
3908 # are in base_branch".
3909 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3910 base_branch]).splitlines()
3911 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07003912 print('Base branch "%s" has %d commits '
3913 'not in this branch.' % (base_branch, len(upstream_commits)))
3914 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003915 return 1
3916
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003917 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003918 svn_head = None
3919 if cmd == 'dcommit' or base_has_submodules:
3920 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
3921 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003922
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003923 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003924 # If the base_head is a submodule merge commit, the first parent of the
3925 # base_head should be a git-svn commit, which is what we're interested in.
3926 base_svn_head = base_branch
3927 if base_has_submodules:
3928 base_svn_head += '^1'
3929
3930 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003931 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07003932 print('This branch has %d additional commits not upstreamed yet.'
3933 % len(extra_commits.splitlines()))
3934 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
3935 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003936 return 1
3937
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003938 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003939 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003940 author = None
3941 if options.contributor:
3942 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003943 hook_results = cl.RunHook(
3944 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003945 may_prompt=not options.force,
3946 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003947 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003948 if not hook_results.should_continue():
3949 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003950
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003951 # Check the tree status if the tree status URL is set.
3952 status = GetTreeStatus()
3953 if 'closed' == status:
3954 print('The tree is closed. Please wait for it to reopen. Use '
3955 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3956 return 1
3957 elif 'unknown' == status:
3958 print('Unable to determine tree status. Please verify manually and '
3959 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3960 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003961
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003962 change_desc = ChangeDescription(options.message)
3963 if not change_desc.description and cl.GetIssue():
3964 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003965
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003966 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00003967 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003968 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00003969 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003970 print('No description set.')
3971 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00003972 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003973
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003974 # Keep a separate copy for the commit message, because the commit message
3975 # contains the link to the Rietveld issue, while the Rietveld message contains
3976 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003977 # Keep a separate copy for the commit message.
3978 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00003979 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003980
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003981 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00003982 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00003983 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00003984 # after it. Add a period on a new line to circumvent this. Also add a space
3985 # before the period to make sure that Gitiles continues to correctly resolve
3986 # the URL.
3987 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003988 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003989 commit_desc.append_footer('Patch from %s.' % options.contributor)
3990
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00003991 print('Description:')
3992 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003993
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003994 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003995 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00003996 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003997
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003998 # We want to squash all this branch's commits into one commit with the proper
3999 # description. We do this by doing a "reset --soft" to the base branch (which
4000 # keeps the working copy the same), then dcommitting that. If origin/master
4001 # has a submodule merge commit, we'll also need to cherry-pick the squashed
4002 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004003 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004004 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4005 # Delete the branches if they exist.
4006 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4007 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4008 result = RunGitWithCode(showref_cmd)
4009 if result[0] == 0:
4010 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004011
4012 # We might be in a directory that's present in this branch but not in the
4013 # trunk. Move up to the top of the tree so that git commands that expect a
4014 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004015 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004016 if rel_base_path:
4017 os.chdir(rel_base_path)
4018
4019 # Stuff our change into the merge branch.
4020 # We wrap in a try...finally block so if anything goes wrong,
4021 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004022 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004023 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004024 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004025 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004026 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00004027 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004028 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004029 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004030 RunGit(
4031 [
4032 'commit', '--author', options.contributor,
4033 '-m', commit_desc.description,
4034 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004035 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004036 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004037 if base_has_submodules:
4038 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
4039 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
4040 RunGit(['checkout', CHERRY_PICK_BRANCH])
4041 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004042 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004043 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004044 mirror = settings.GetGitMirror(remote)
4045 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004046 pending_prefix = settings.GetPendingRefPrefix()
4047 if not pending_prefix or branch.startswith(pending_prefix):
4048 # If not using refs/pending/heads/* at all, or target ref is already set
4049 # to pending, then push to the target ref directly.
4050 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004051 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004052 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004053 else:
4054 # Cherry-pick the change on top of pending ref and then push it.
4055 assert branch.startswith('refs/'), branch
4056 assert pending_prefix[-1] == '/', pending_prefix
4057 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004058 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004059 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004060 if retcode == 0:
4061 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004062 else:
4063 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004064 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00004065 'svn', 'dcommit',
4066 '-C%s' % options.similarity,
4067 '--no-rebase', '--rmdir',
4068 ]
4069 if settings.GetForceHttpsCommitUrl():
4070 # Allow forcing https commit URLs for some projects that don't allow
4071 # committing to http URLs (like Google Code).
4072 remote_url = cl.GetGitSvnRemoteUrl()
4073 if urlparse.urlparse(remote_url).scheme == 'http':
4074 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004075 cmd_args.append('--commit-url=%s' % remote_url)
4076 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004077 if 'Committed r' in output:
4078 revision = re.match(
4079 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4080 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004081 finally:
4082 # And then swap back to the original branch and clean up.
4083 RunGit(['checkout', '-q', cl.GetBranch()])
4084 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004085 if base_has_submodules:
4086 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004087
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004088 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004089 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004090 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004091
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004092 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004093 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004094 try:
4095 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4096 # We set pushed_to_pending to False, since it made it all the way to the
4097 # real ref.
4098 pushed_to_pending = False
4099 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004100 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004101
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004102 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004103 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004104 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004105 if not to_pending:
4106 if viewvc_url and revision:
4107 change_desc.append_footer(
4108 'Committed: %s%s' % (viewvc_url, revision))
4109 elif revision:
4110 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004111 print('Closing issue '
4112 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004113 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004114 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004115 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004116 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004117 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004118 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004119 if options.bypass_hooks:
4120 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4121 else:
4122 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004123 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004124
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004125 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004126 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004127 print('The commit is in the pending queue (%s).' % pending_ref)
4128 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4129 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004130
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004131 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4132 if os.path.isfile(hook):
4133 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004134
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004135 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004136
4137
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004138def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004139 print()
4140 print('Waiting for commit to be landed on %s...' % real_ref)
4141 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004142 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4143 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004144 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004145
4146 loop = 0
4147 while True:
4148 sys.stdout.write('fetching (%d)... \r' % loop)
4149 sys.stdout.flush()
4150 loop += 1
4151
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004152 if mirror:
4153 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004154 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4155 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4156 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4157 for commit in commits.splitlines():
4158 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004159 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004160 return commit
4161
4162 current_rev = to_rev
4163
4164
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004165def PushToGitPending(remote, pending_ref, upstream_ref):
4166 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4167
4168 Returns:
4169 (retcode of last operation, output log of last operation).
4170 """
4171 assert pending_ref.startswith('refs/'), pending_ref
4172 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4173 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4174 code = 0
4175 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004176 max_attempts = 3
4177 attempts_left = max_attempts
4178 while attempts_left:
4179 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004180 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004181 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004182
4183 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004184 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004185 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004186 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004187 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004188 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004189 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004190 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004191 continue
4192
4193 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004194 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004195 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004196 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004197 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004198 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4199 'the following files have merge conflicts:' % pending_ref)
4200 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4201 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004202 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004203 return code, out
4204
4205 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004206 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004207 code, out = RunGitWithCode(
4208 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4209 if code == 0:
4210 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004211 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004212 return code, out
4213
vapiera7fbd5a2016-06-16 09:17:49 -07004214 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004215 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004216 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004217 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004218 print('Fatal push error. Make sure your .netrc credentials and git '
4219 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004220 return code, out
4221
vapiera7fbd5a2016-06-16 09:17:49 -07004222 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004223 return code, out
4224
4225
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004226def IsFatalPushFailure(push_stdout):
4227 """True if retrying push won't help."""
4228 return '(prohibited by Gerrit)' in push_stdout
4229
4230
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004231@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004232def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004233 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004234 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004235 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004236 # If it looks like previous commits were mirrored with git-svn.
4237 message = """This repository appears to be a git-svn mirror, but no
4238upstream SVN master is set. You probably need to run 'git auto-svn' once."""
4239 else:
4240 message = """This doesn't appear to be an SVN repository.
4241If your project has a true, writeable git repository, you probably want to run
4242'git cl land' instead.
4243If your project has a git mirror of an upstream SVN master, you probably need
4244to run 'git svn init'.
4245
4246Using the wrong command might cause your commit to appear to succeed, and the
4247review to be closed, without actually landing upstream. If you choose to
4248proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004249 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004250 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
tandrii3bb82ff2016-06-17 07:36:36 -07004251 # TODO(tandrii): kill this post SVN migration with
4252 # https://codereview.chromium.org/2076683002
4253 print('WARNING: chrome infrastructure is migrating SVN repos to Git.\n'
4254 'Please let us know of this project you are committing to:'
4255 ' http://crbug.com/600451')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004256 return SendUpstream(parser, args, 'dcommit')
4257
4258
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004259@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004260def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004261 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004262 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004263 print('This appears to be an SVN repository.')
4264 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004265 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004266 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004267 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004268
4269
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004270@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004271def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004272 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004273 parser.add_option('-b', dest='newbranch',
4274 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004275 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004276 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004277 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4278 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004279 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004280 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004281 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004282 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004283 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004284 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004285
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004286
4287 group = optparse.OptionGroup(
4288 parser,
4289 'Options for continuing work on the current issue uploaded from a '
4290 'different clone (e.g. different machine). Must be used independently '
4291 'from the other options. No issue number should be specified, and the '
4292 'branch must have an issue number associated with it')
4293 group.add_option('--reapply', action='store_true', dest='reapply',
4294 help='Reset the branch and reapply the issue.\n'
4295 'CAUTION: This will undo any local changes in this '
4296 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004297
4298 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004299 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004300 parser.add_option_group(group)
4301
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004302 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004303 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004304 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004305 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004306 auth_config = auth.extract_auth_config_from_options(options)
4307
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004308
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004309 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004310 if options.newbranch:
4311 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004312 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004313 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004314
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004315 cl = Changelist(auth_config=auth_config,
4316 codereview=options.forced_codereview)
4317 if not cl.GetIssue():
4318 parser.error('current branch must have an associated issue')
4319
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004320 upstream = cl.GetUpstreamBranch()
4321 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004322 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004323
4324 RunGit(['reset', '--hard', upstream])
4325 if options.pull:
4326 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004327
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004328 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4329 options.directory)
4330
4331 if len(args) != 1 or not args[0]:
4332 parser.error('Must specify issue number or url')
4333
4334 # We don't want uncommitted changes mixed up with the patch.
4335 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004336 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004337
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004338 if options.newbranch:
4339 if options.force:
4340 RunGit(['branch', '-D', options.newbranch],
4341 stderr=subprocess2.PIPE, error_ok=True)
4342 RunGit(['new-branch', options.newbranch])
4343
4344 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4345
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004346 if cl.IsGerrit():
4347 if options.reject:
4348 parser.error('--reject is not supported with Gerrit codereview.')
4349 if options.nocommit:
4350 parser.error('--nocommit is not supported with Gerrit codereview.')
4351 if options.directory:
4352 parser.error('--directory is not supported with Gerrit codereview.')
4353
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004354 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004355 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004356
4357
4358def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004359 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004360 # Provide a wrapper for git svn rebase to help avoid accidental
4361 # git svn dcommit.
4362 # It's the only command that doesn't use parser at all since we just defer
4363 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004364
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004365 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004366
4367
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004368def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004369 """Fetches the tree status and returns either 'open', 'closed',
4370 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004371 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004372 if url:
4373 status = urllib2.urlopen(url).read().lower()
4374 if status.find('closed') != -1 or status == '0':
4375 return 'closed'
4376 elif status.find('open') != -1 or status == '1':
4377 return 'open'
4378 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004379 return 'unset'
4380
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004381
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004382def GetTreeStatusReason():
4383 """Fetches the tree status from a json url and returns the message
4384 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004385 url = settings.GetTreeStatusUrl()
4386 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004387 connection = urllib2.urlopen(json_url)
4388 status = json.loads(connection.read())
4389 connection.close()
4390 return status['message']
4391
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004392
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004393def GetBuilderMaster(bot_list):
4394 """For a given builder, fetch the master from AE if available."""
4395 map_url = 'https://builders-map.appspot.com/'
4396 try:
4397 master_map = json.load(urllib2.urlopen(map_url))
4398 except urllib2.URLError as e:
4399 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4400 (map_url, e))
4401 except ValueError as e:
4402 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4403 if not master_map:
4404 return None, 'Failed to build master map.'
4405
4406 result_master = ''
4407 for bot in bot_list:
4408 builder = bot.split(':', 1)[0]
4409 master_list = master_map.get(builder, [])
4410 if not master_list:
4411 return None, ('No matching master for builder %s.' % builder)
4412 elif len(master_list) > 1:
4413 return None, ('The builder name %s exists in multiple masters %s.' %
4414 (builder, master_list))
4415 else:
4416 cur_master = master_list[0]
4417 if not result_master:
4418 result_master = cur_master
4419 elif result_master != cur_master:
4420 return None, 'The builders do not belong to the same master.'
4421 return result_master, None
4422
4423
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004424def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004425 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004426 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004427 status = GetTreeStatus()
4428 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004429 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004430 return 2
4431
vapiera7fbd5a2016-06-16 09:17:49 -07004432 print('The tree is %s' % status)
4433 print()
4434 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004435 if status != 'open':
4436 return 1
4437 return 0
4438
4439
maruel@chromium.org15192402012-09-06 12:38:29 +00004440def CMDtry(parser, args):
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004441 """Triggers try jobs through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00004442 group = optparse.OptionGroup(parser, "Try job options")
4443 group.add_option(
4444 "-b", "--bot", action="append",
4445 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
4446 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004447 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00004448 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004449 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00004450 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004451 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00004452 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004453 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004454 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004455 "-r", "--revision",
4456 help="Revision to use for the try job; default: the "
4457 "revision will be determined by the try server; see "
4458 "its waterfall for more info")
4459 group.add_option(
4460 "-c", "--clobber", action="store_true", default=False,
4461 help="Force a clobber before building; e.g. don't do an "
4462 "incremental build")
4463 group.add_option(
4464 "--project",
4465 help="Override which project to use. Projects are defined "
4466 "server-side to define what default bot set to use")
4467 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004468 "-p", "--property", dest="properties", action="append", default=[],
4469 help="Specify generic properties in the form -p key1=value1 -p "
4470 "key2=value2 etc (buildbucket only). The value will be treated as "
4471 "json if decodable, or as string otherwise.")
4472 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004473 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004474 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004475 "--use-rietveld", action="store_true", default=False,
4476 help="Use Rietveld to trigger try jobs.")
4477 group.add_option(
4478 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4479 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004480 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004481 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004482 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004483 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004484
machenbach@chromium.org45453142015-09-15 08:45:22 +00004485 if options.use_rietveld and options.properties:
4486 parser.error('Properties can only be specified with buildbucket')
4487
4488 # Make sure that all properties are prop=value pairs.
4489 bad_params = [x for x in options.properties if '=' not in x]
4490 if bad_params:
4491 parser.error('Got properties with missing "=": %s' % bad_params)
4492
maruel@chromium.org15192402012-09-06 12:38:29 +00004493 if args:
4494 parser.error('Unknown arguments: %s' % args)
4495
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004496 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004497 if not cl.GetIssue():
4498 parser.error('Need to upload first')
4499
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004500 if cl.IsGerrit():
4501 parser.error(
4502 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4503 'If your project has Commit Queue, dry run is a workaround:\n'
4504 ' git cl set-commit --dry-run')
4505 # Code below assumes Rietveld issue.
4506 # TODO(tandrii): actually implement for Gerrit http://crbug.com/599931.
4507
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004508 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004509 if props.get('closed'):
4510 parser.error('Cannot send tryjobs for a closed CL')
4511
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004512 if props.get('private'):
4513 parser.error('Cannot use trybots with private issue')
4514
maruel@chromium.org15192402012-09-06 12:38:29 +00004515 if not options.name:
4516 options.name = cl.GetBranch()
4517
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004518 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004519 options.master, err_msg = GetBuilderMaster(options.bot)
4520 if err_msg:
4521 parser.error('Tryserver master cannot be found because: %s\n'
4522 'Please manually specify the tryserver master'
4523 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004524
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004525 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004526 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004527 if not options.bot:
4528 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004529
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004530 # Get try masters from PRESUBMIT.py files.
4531 masters = presubmit_support.DoGetTryMasters(
4532 change,
4533 change.LocalPaths(),
4534 settings.GetRoot(),
4535 None,
4536 None,
4537 options.verbose,
4538 sys.stdout)
4539 if masters:
4540 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004541
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004542 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4543 options.bot = presubmit_support.DoGetTrySlaves(
4544 change,
4545 change.LocalPaths(),
4546 settings.GetRoot(),
4547 None,
4548 None,
4549 options.verbose,
4550 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004551
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004552 if not options.bot:
tandrii9de9ec62016-07-13 03:01:59 -07004553 return {}
maruel@chromium.org15192402012-09-06 12:38:29 +00004554
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004555 builders_and_tests = {}
4556 # TODO(machenbach): The old style command-line options don't support
4557 # multiple try masters yet.
4558 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4559 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4560
4561 for bot in old_style:
4562 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004563 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004564 elif ',' in bot:
4565 parser.error('Specify one bot per --bot flag')
4566 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004567 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004568
4569 for bot, tests in new_style:
4570 builders_and_tests.setdefault(bot, []).extend(tests)
4571
4572 # Return a master map with one master to be backwards compatible. The
4573 # master name defaults to an empty string, which will cause the master
4574 # not to be set on rietveld (deprecated).
4575 return {options.master: builders_and_tests}
4576
4577 masters = GetMasterMap()
tandrii9de9ec62016-07-13 03:01:59 -07004578 if not masters:
4579 # Default to triggering Dry Run (see http://crbug.com/625697).
4580 if options.verbose:
4581 print('git cl try with no bots now defaults to CQ Dry Run.')
4582 try:
4583 cl.SetCQState(_CQState.DRY_RUN)
4584 print('scheduled CQ Dry Run on %s' % cl.GetIssueURL())
4585 return 0
4586 except KeyboardInterrupt:
4587 raise
4588 except:
4589 print('WARNING: failed to trigger CQ Dry Run.\n'
4590 'Either:\n'
4591 ' * your project has no CQ\n'
4592 ' * you don\'t have permission to trigger Dry Run\n'
4593 ' * bug in this code (see stack trace below).\n'
4594 'Consider specifying which bots to trigger manually '
4595 'or asking your project owners for permissions '
4596 'or contacting Chrome Infrastructure team at '
4597 'https://www.chromium.org/infra\n\n')
4598 # Still raise exception so that stack trace is printed.
4599 raise
stip@chromium.org43064fd2013-12-18 20:07:44 +00004600
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004601 for builders in masters.itervalues():
4602 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004603 print('ERROR You are trying to send a job to a triggered bot. This type '
4604 'of bot requires an\ninitial job from a parent (usually a builder).'
4605 ' Instead send your job to the parent.\n'
4606 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004607 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004608
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004609 patchset = cl.GetMostRecentPatchset()
4610 if patchset and patchset != cl.GetPatchset():
4611 print(
4612 '\nWARNING Mismatch between local config and server. Did a previous '
4613 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4614 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004615 if options.luci:
4616 trigger_luci_job(cl, masters, options)
4617 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004618 try:
4619 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4620 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004621 print('ERROR: %s' % ex)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004622 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004623 except Exception as e:
4624 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
vapiera7fbd5a2016-06-16 09:17:49 -07004625 print('ERROR: Exception when trying to trigger tryjobs: %s\n%s' %
4626 (e, stacktrace))
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004627 return 1
4628 else:
4629 try:
4630 cl.RpcServer().trigger_distributed_try_jobs(
4631 cl.GetIssue(), patchset, options.name, options.clobber,
4632 options.revision, masters)
4633 except urllib2.HTTPError as e:
4634 if e.code == 404:
4635 print('404 from rietveld; '
4636 'did you mean to use "git try" instead of "git cl try"?')
4637 return 1
4638 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004639
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004640 for (master, builders) in sorted(masters.iteritems()):
4641 if master:
vapiera7fbd5a2016-06-16 09:17:49 -07004642 print('Master: %s' % master)
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004643 length = max(len(builder) for builder in builders)
4644 for builder in sorted(builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004645 print(' %*s: %s' % (length, builder, ','.join(builders[builder])))
maruel@chromium.org15192402012-09-06 12:38:29 +00004646 return 0
4647
4648
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004649def CMDtry_results(parser, args):
4650 group = optparse.OptionGroup(parser, "Try job results options")
4651 group.add_option(
4652 "-p", "--patchset", type=int, help="patchset number if not current.")
4653 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004654 "--print-master", action='store_true', help="print master name as well.")
4655 group.add_option(
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004656 "--color", action='store_true', default=setup_color.IS_TTY,
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004657 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004658 group.add_option(
4659 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4660 help="Host of buildbucket. The default host is %default.")
4661 parser.add_option_group(group)
4662 auth.add_auth_options(parser)
4663 options, args = parser.parse_args(args)
4664 if args:
4665 parser.error('Unrecognized args: %s' % ' '.join(args))
4666
4667 auth_config = auth.extract_auth_config_from_options(options)
4668 cl = Changelist(auth_config=auth_config)
4669 if not cl.GetIssue():
4670 parser.error('Need to upload first')
4671
4672 if not options.patchset:
4673 options.patchset = cl.GetMostRecentPatchset()
4674 if options.patchset and options.patchset != cl.GetPatchset():
4675 print(
4676 '\nWARNING Mismatch between local config and server. Did a previous '
4677 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4678 'Continuing using\npatchset %s.\n' % options.patchset)
4679 try:
4680 jobs = fetch_try_jobs(auth_config, cl, options)
4681 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004682 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004683 return 1
4684 except Exception as e:
4685 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
vapiera7fbd5a2016-06-16 09:17:49 -07004686 print('ERROR: Exception when trying to fetch tryjobs: %s\n%s' %
4687 (e, stacktrace))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004688 return 1
4689 print_tryjobs(options, jobs)
4690 return 0
4691
4692
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004693@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004694def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004695 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004696 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004697 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004698 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004699
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004700 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004701 if args:
4702 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004703 branch = cl.GetBranch()
4704 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004705 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004706 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004707
4708 # Clear configured merge-base, if there is one.
4709 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004710 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004711 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004712 return 0
4713
4714
thestig@chromium.org00858c82013-12-02 23:08:03 +00004715def CMDweb(parser, args):
4716 """Opens the current CL in the web browser."""
4717 _, args = parser.parse_args(args)
4718 if args:
4719 parser.error('Unrecognized args: %s' % ' '.join(args))
4720
4721 issue_url = Changelist().GetIssueURL()
4722 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004723 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004724 return 1
4725
4726 webbrowser.open(issue_url)
4727 return 0
4728
4729
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004730def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004731 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004732 parser.add_option('-d', '--dry-run', action='store_true',
4733 help='trigger in dry run mode')
4734 parser.add_option('-c', '--clear', action='store_true',
4735 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004736 auth.add_auth_options(parser)
4737 options, args = parser.parse_args(args)
4738 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004739 if args:
4740 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004741 if options.dry_run and options.clear:
4742 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4743
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004744 cl = Changelist(auth_config=auth_config)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004745 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07004746 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004747 elif options.dry_run:
4748 state = _CQState.DRY_RUN
4749 else:
4750 state = _CQState.COMMIT
4751 if not cl.GetIssue():
4752 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07004753 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004754 return 0
4755
4756
groby@chromium.org411034a2013-02-26 15:12:01 +00004757def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004758 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004759 auth.add_auth_options(parser)
4760 options, args = parser.parse_args(args)
4761 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004762 if args:
4763 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004764 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00004765 # Ensure there actually is an issue to close.
4766 cl.GetDescription()
4767 cl.CloseIssue()
4768 return 0
4769
4770
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004771def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004772 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004773 auth.add_auth_options(parser)
4774 options, args = parser.parse_args(args)
4775 auth_config = auth.extract_auth_config_from_options(options)
4776 if args:
4777 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004778
4779 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004780 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004781 # Staged changes would be committed along with the patch from last
4782 # upload, hence counted toward the "last upload" side in the final
4783 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004784 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004785 return 1
4786
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004787 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004788 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004789 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004790 if not issue:
4791 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004792 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004793 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004794
4795 # Create a new branch based on the merge-base
4796 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004797 # Clear cached branch in cl object, to avoid overwriting original CL branch
4798 # properties.
4799 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004800 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004801 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004802 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004803 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004804 return rtn
4805
wychen@chromium.org06928532015-02-03 02:11:29 +00004806 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004807 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00004808 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004809 finally:
4810 RunGit(['checkout', '-q', branch])
4811 RunGit(['branch', '-D', TMP_BRANCH])
4812
4813 return 0
4814
4815
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004816def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004817 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004818 parser.add_option(
4819 '--no-color',
4820 action='store_true',
4821 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004822 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004823 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004824 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004825
4826 author = RunGit(['config', 'user.email']).strip() or None
4827
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004828 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004829
4830 if args:
4831 if len(args) > 1:
4832 parser.error('Unknown args')
4833 base_branch = args[0]
4834 else:
4835 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004836 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004837
4838 change = cl.GetChange(base_branch, None)
4839 return owners_finder.OwnersFinder(
4840 [f.LocalPath() for f in
4841 cl.GetChange(base_branch, None).AffectedFiles()],
4842 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07004843 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004844 disable_color=options.no_color).run()
4845
4846
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004847def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004848 """Generates a diff command."""
4849 # Generate diff for the current branch's changes.
4850 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4851 upstream_commit, '--' ]
4852
4853 if args:
4854 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004855 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004856 diff_cmd.append(arg)
4857 else:
4858 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004859
4860 return diff_cmd
4861
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004862def MatchingFileType(file_name, extensions):
4863 """Returns true if the file name ends with one of the given extensions."""
4864 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004865
enne@chromium.org555cfe42014-01-29 18:21:39 +00004866@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004867def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004868 """Runs auto-formatting tools (clang-format etc.) on the diff."""
zengsterbf470142016-07-07 16:43:00 -07004869 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07004870 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004871 parser.add_option('--full', action='store_true',
4872 help='Reformat the full content of all touched files')
4873 parser.add_option('--dry-run', action='store_true',
4874 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004875 parser.add_option('--python', action='store_true',
4876 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004877 parser.add_option('--diff', action='store_true',
4878 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004879 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004880
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004881 # git diff generates paths against the root of the repository. Change
4882 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004883 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004884 if rel_base_path:
4885 os.chdir(rel_base_path)
4886
digit@chromium.org29e47272013-05-17 17:01:46 +00004887 # Grab the merge-base commit, i.e. the upstream commit of the current
4888 # branch when it was created or the last time it was rebased. This is
4889 # to cover the case where the user may have called "git fetch origin",
4890 # moving the origin branch to a newer commit, but hasn't rebased yet.
4891 upstream_commit = None
4892 cl = Changelist()
4893 upstream_branch = cl.GetUpstreamBranch()
4894 if upstream_branch:
4895 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4896 upstream_commit = upstream_commit.strip()
4897
4898 if not upstream_commit:
4899 DieWithError('Could not find base commit for this branch. '
4900 'Are you in detached state?')
4901
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004902 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4903 diff_output = RunGit(changed_files_cmd)
4904 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004905 # Filter out files deleted by this CL
4906 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004907
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004908 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4909 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4910 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004911 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004912
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004913 top_dir = os.path.normpath(
4914 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
4915
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004916 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4917 # formatted. This is used to block during the presubmit.
4918 return_value = 0
4919
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004920 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004921 # Locate the clang-format binary in the checkout
4922 try:
4923 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07004924 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004925 DieWithError(e)
4926
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004927 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004928 cmd = [clang_format_tool]
4929 if not opts.dry_run and not opts.diff:
4930 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004931 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004932 if opts.diff:
4933 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004934 else:
4935 env = os.environ.copy()
4936 env['PATH'] = str(os.path.dirname(clang_format_tool))
4937 try:
4938 script = clang_format.FindClangFormatScriptInChromiumTree(
4939 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07004940 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004941 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004942
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004943 cmd = [sys.executable, script, '-p0']
4944 if not opts.dry_run and not opts.diff:
4945 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004946
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004947 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
4948 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004949
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004950 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
4951 if opts.diff:
4952 sys.stdout.write(stdout)
4953 if opts.dry_run and len(stdout) > 0:
4954 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004955
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004956 # Similar code to above, but using yapf on .py files rather than clang-format
4957 # on C/C++ files
4958 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004959 yapf_tool = gclient_utils.FindExecutable('yapf')
4960 if yapf_tool is None:
4961 DieWithError('yapf not found in PATH')
4962
4963 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004964 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004965 cmd = [yapf_tool]
4966 if not opts.dry_run and not opts.diff:
4967 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004968 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004969 if opts.diff:
4970 sys.stdout.write(stdout)
4971 else:
4972 # TODO(sbc): yapf --lines mode still has some issues.
4973 # https://github.com/google/yapf/issues/154
4974 DieWithError('--python currently only works with --full')
4975
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004976 # Dart's formatter does not have the nice property of only operating on
4977 # modified chunks, so hard code full.
4978 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004979 try:
4980 command = [dart_format.FindDartFmtToolInChromiumTree()]
4981 if not opts.dry_run and not opts.diff:
4982 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004983 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004984
ppi@chromium.org6593d932016-03-03 15:41:15 +00004985 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004986 if opts.dry_run and stdout:
4987 return_value = 2
4988 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07004989 print('Warning: Unable to check Dart code formatting. Dart SDK not '
4990 'found in this checkout. Files in other languages are still '
4991 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004992
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004993 # Format GN build files. Always run on full build files for canonical form.
4994 if gn_diff_files:
4995 cmd = ['gn', 'format']
4996 if not opts.dry_run and not opts.diff:
4997 cmd.append('--in-place')
4998 for gn_diff_file in gn_diff_files:
bsep@chromium.org627d9002016-04-29 00:00:52 +00004999 stdout = RunCommand(cmd + [gn_diff_file],
5000 shell=sys.platform == 'win32',
5001 cwd=top_dir)
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005002 if opts.diff:
5003 sys.stdout.write(stdout)
5004
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005005 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005006
5007
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005008@subcommand.usage('<codereview url or issue id>')
5009def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005010 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005011 _, args = parser.parse_args(args)
5012
5013 if len(args) != 1:
5014 parser.print_help()
5015 return 1
5016
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005017 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005018 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005019 parser.print_help()
5020 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005021 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005022
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005023 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005024 output = RunGit(['config', '--local', '--get-regexp',
5025 r'branch\..*\.%s' % issueprefix],
5026 error_ok=True)
5027 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005028 if issue == target_issue:
5029 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005030
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005031 branches = []
5032 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00005033 branches.extend(find_issues(cls.IssueSettingSuffix()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005034 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005035 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005036 return 1
5037 if len(branches) == 1:
5038 RunGit(['checkout', branches[0]])
5039 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005040 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005041 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005042 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005043 which = raw_input('Choose by index: ')
5044 try:
5045 RunGit(['checkout', branches[int(which)]])
5046 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005047 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005048 return 1
5049
5050 return 0
5051
5052
maruel@chromium.org29404b52014-09-08 22:58:00 +00005053def CMDlol(parser, args):
5054 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005055 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005056 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5057 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5058 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005059 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005060 return 0
5061
5062
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005063class OptionParser(optparse.OptionParser):
5064 """Creates the option parse and add --verbose support."""
5065 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005066 optparse.OptionParser.__init__(
5067 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005068 self.add_option(
5069 '-v', '--verbose', action='count', default=0,
5070 help='Use 2 times for more debugging info')
5071
5072 def parse_args(self, args=None, values=None):
5073 options, args = optparse.OptionParser.parse_args(self, args, values)
5074 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5075 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5076 return options, args
5077
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005078
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005079def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005080 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005081 print('\nYour python version %s is unsupported, please upgrade.\n' %
5082 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005083 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005084
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005085 # Reload settings.
5086 global settings
5087 settings = Settings()
5088
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005089 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005090 dispatcher = subcommand.CommandDispatcher(__name__)
5091 try:
5092 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005093 except auth.AuthenticationError as e:
5094 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005095 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005096 if e.code != 500:
5097 raise
5098 DieWithError(
5099 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5100 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005101 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005102
5103
5104if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005105 # These affect sys.stdout so do it outside of main() to simplify mocks in
5106 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005107 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005108 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005109 try:
5110 sys.exit(main(sys.argv[1:]))
5111 except KeyboardInterrupt:
5112 sys.stderr.write('interrupted\n')
5113 sys.exit(1)