blob: 5d91536dc7184517052591f9a67d4605cef09806 [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00008"""A git-command for integrating reviews on Rietveld and Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
vapiera7fbd5a2016-06-16 09:17:49 -070010from __future__ import print_function
11
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000012from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000013from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000014import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000015import collections
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000016import glob
sheyang@google.com6ebaf782015-05-12 19:17:54 +000017import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000018import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000020import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import optparse
22import os
23import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000024import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000026import textwrap
sheyang@google.com6ebaf782015-05-12 19:17:54 +000027import time
28import traceback
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000029import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000030import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000031import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000032import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000033import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000034import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000035
36try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000037 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000038except ImportError:
39 pass
40
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000041from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000042from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000043from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000044import auth
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +000045from luci_hacks import trigger_luci_job as luci_trigger
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000046import clang_format
tandrii@chromium.org71184c02016-01-13 15:18:44 +000047import commit_queue
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000048import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000049import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000050import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000051import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000052import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000053import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000054import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000055import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000056import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000057import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000058import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000059import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000060import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000061import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000062import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000063import watchlists
64
tandrii7400cf02016-06-21 08:48:07 -070065__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000066
tandrii9d2c7a32016-06-22 03:42:45 -070067COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000068DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000069POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000070DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000071GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
rmistry@google.comc68112d2015-03-03 12:48:06 +000072REFS_THAT_ALIAS_TO_OTHER_REFS = {
73 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
74 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
75}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000076
thestig@chromium.org44202a22014-03-11 19:22:18 +000077# Valid extensions for files we want to lint.
78DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
79DEFAULT_LINT_IGNORE_REGEX = r"$^"
80
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000081# Shortcut since it quickly becomes redundant.
82Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000083
maruel@chromium.orgddd59412011-11-30 14:20:38 +000084# Initialized in main()
85settings = None
86
87
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000088def DieWithError(message):
vapiera7fbd5a2016-06-16 09:17:49 -070089 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000090 sys.exit(1)
91
92
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000093def GetNoGitPagerEnv():
94 env = os.environ.copy()
95 # 'cat' is a magical git string that disables pagers on all platforms.
96 env['GIT_PAGER'] = 'cat'
97 return env
98
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000099
bsep@chromium.org627d9002016-04-29 00:00:52 +0000100def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000101 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000102 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000103 except subprocess2.CalledProcessError as e:
104 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000105 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000106 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000107 'Command "%s" failed.\n%s' % (
108 ' '.join(args), error_message or e.stdout or ''))
109 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000110
111
112def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000113 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000114 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000115
116
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000117def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000118 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000119 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000120 if suppress_stderr:
121 stderr = subprocess2.VOID
122 else:
123 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000124 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000125 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000126 stdout=subprocess2.PIPE,
127 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000128 return code, out[0]
129 except ValueError:
130 # When the subprocess fails, it returns None. That triggers a ValueError
131 # when trying to unpack the return value into (out, code).
132 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000133
134
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000135def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000136 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000137 return RunGitWithCode(args, suppress_stderr=True)[1]
138
139
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000140def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000141 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000142 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000143 return (version.startswith(prefix) and
144 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000145
146
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000147def BranchExists(branch):
148 """Return True if specified branch exists."""
149 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
150 suppress_stderr=True)
151 return not code
152
153
maruel@chromium.org90541732011-04-01 17:54:18 +0000154def ask_for_data(prompt):
155 try:
156 return raw_input(prompt)
157 except KeyboardInterrupt:
158 # Hide the exception.
159 sys.exit(1)
160
161
iannucci@chromium.org79540052012-10-19 23:15:26 +0000162def git_set_branch_value(key, value):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000163 branch = GetCurrentBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000164 if not branch:
165 return
166
167 cmd = ['config']
168 if isinstance(value, int):
169 cmd.append('--int')
170 git_key = 'branch.%s.%s' % (branch, key)
171 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000172
173
174def git_get_branch_default(key, default):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000175 branch = GetCurrentBranch()
iannucci@chromium.org79540052012-10-19 23:15:26 +0000176 if branch:
177 git_key = 'branch.%s.%s' % (branch, key)
178 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
179 try:
180 return int(stdout.strip())
181 except ValueError:
182 pass
183 return default
184
185
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000186def add_git_similarity(parser):
187 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000188 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000189 help='Sets the percentage that a pair of files need to match in order to'
190 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000191 parser.add_option(
192 '--find-copies', action='store_true',
193 help='Allows git to look for copies.')
194 parser.add_option(
195 '--no-find-copies', action='store_false', dest='find_copies',
196 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000197
198 old_parser_args = parser.parse_args
199 def Parse(args):
200 options, args = old_parser_args(args)
201
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000202 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000203 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000204 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000205 print('Note: Saving similarity of %d%% in git config.'
206 % options.similarity)
207 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000208
iannucci@chromium.org79540052012-10-19 23:15:26 +0000209 options.similarity = max(0, min(options.similarity, 100))
210
211 if options.find_copies is None:
212 options.find_copies = bool(
213 git_get_branch_default('git-find-copies', True))
214 else:
215 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000216
217 print('Using %d%% similarity for rename/copy detection. '
218 'Override with --similarity.' % options.similarity)
219
220 return options, args
221 parser.parse_args = Parse
222
223
machenbach@chromium.org45453142015-09-15 08:45:22 +0000224def _get_properties_from_options(options):
225 properties = dict(x.split('=', 1) for x in options.properties)
226 for key, val in properties.iteritems():
227 try:
228 properties[key] = json.loads(val)
229 except ValueError:
230 pass # If a value couldn't be evaluated, treat it as a string.
231 return properties
232
233
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000234def _prefix_master(master):
235 """Convert user-specified master name to full master name.
236
237 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
238 name, while the developers always use shortened master name
239 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
240 function does the conversion for buildbucket migration.
241 """
242 prefix = 'master.'
243 if master.startswith(prefix):
244 return master
245 return '%s%s' % (prefix, master)
246
247
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000248def _buildbucket_retry(operation_name, http, *args, **kwargs):
249 """Retries requests to buildbucket service and returns parsed json content."""
250 try_count = 0
251 while True:
252 response, content = http.request(*args, **kwargs)
253 try:
254 content_json = json.loads(content)
255 except ValueError:
256 content_json = None
257
258 # Buildbucket could return an error even if status==200.
259 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000260 error = content_json.get('error')
261 if error.get('code') == 403:
262 raise BuildbucketResponseException(
263 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000264 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000265 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000266 raise BuildbucketResponseException(msg)
267
268 if response.status == 200:
269 if not content_json:
270 raise BuildbucketResponseException(
271 'Buildbucket returns invalid json content: %s.\n'
272 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
273 content)
274 return content_json
275 if response.status < 500 or try_count >= 2:
276 raise httplib2.HttpLib2Error(content)
277
278 # status >= 500 means transient failures.
279 logging.debug('Transient errors when %s. Will retry.', operation_name)
280 time.sleep(0.5 + 1.5*try_count)
281 try_count += 1
282 assert False, 'unreachable'
283
284
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000285def trigger_luci_job(changelist, masters, options):
286 """Send a job to run on LUCI."""
287 issue_props = changelist.GetIssueProperties()
288 issue = changelist.GetIssue()
289 patchset = changelist.GetMostRecentPatchset()
290 for builders_and_tests in sorted(masters.itervalues()):
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000291 # TODO(hinoka et al): add support for other properties.
292 # Currently, this completely ignores testfilter and other properties.
293 for builder in sorted(builders_and_tests):
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000294 luci_trigger.trigger(
295 builder, 'HEAD', issue, patchset, issue_props['project'])
296
297
machenbach@chromium.org45453142015-09-15 08:45:22 +0000298def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000299 rietveld_url = settings.GetDefaultServerUrl()
300 rietveld_host = urlparse.urlparse(rietveld_url).hostname
301 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
302 http = authenticator.authorize(httplib2.Http())
303 http.force_exception_to_status_code = True
304 issue_props = changelist.GetIssueProperties()
305 issue = changelist.GetIssue()
306 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000307 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000308
309 buildbucket_put_url = (
310 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000311 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000312 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
313 hostname=rietveld_host,
314 issue=issue,
315 patch=patchset)
316
317 batch_req_body = {'builds': []}
318 print_text = []
319 print_text.append('Tried jobs on:')
320 for master, builders_and_tests in sorted(masters.iteritems()):
321 print_text.append('Master: %s' % master)
322 bucket = _prefix_master(master)
323 for builder, tests in sorted(builders_and_tests.iteritems()):
324 print_text.append(' %s: %s' % (builder, tests))
325 parameters = {
326 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000327 'changes': [{
328 'author': {'email': issue_props['owner_email']},
329 'revision': options.revision,
330 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000331 'properties': {
332 'category': category,
333 'issue': issue,
334 'master': master,
335 'patch_project': issue_props['project'],
336 'patch_storage': 'rietveld',
337 'patchset': patchset,
338 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000339 'rietveld': rietveld_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000340 },
341 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000342 if 'presubmit' in builder.lower():
343 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000344 if tests:
345 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000346 if properties:
347 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000348 if options.clobber:
349 parameters['properties']['clobber'] = True
350 batch_req_body['builds'].append(
351 {
352 'bucket': bucket,
353 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000354 'client_operation_id': str(uuid.uuid4()),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000355 'tags': ['builder:%s' % builder,
356 'buildset:%s' % buildset,
357 'master:%s' % master,
358 'user_agent:git_cl_try']
359 }
360 )
361
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000362 _buildbucket_retry(
363 'triggering tryjobs',
364 http,
365 buildbucket_put_url,
366 'PUT',
367 body=json.dumps(batch_req_body),
368 headers={'Content-Type': 'application/json'}
369 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000370 print_text.append('To see results here, run: git cl try-results')
371 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700372 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000373
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000374
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000375def fetch_try_jobs(auth_config, changelist, options):
376 """Fetches tryjobs from buildbucket.
377
378 Returns a map from build id to build info as json dictionary.
379 """
380 rietveld_url = settings.GetDefaultServerUrl()
381 rietveld_host = urlparse.urlparse(rietveld_url).hostname
382 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
383 if authenticator.has_cached_credentials():
384 http = authenticator.authorize(httplib2.Http())
385 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700386 print('Warning: Some results might be missing because %s' %
387 # Get the message on how to login.
388 (auth.LoginRequiredError(rietveld_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000389 http = httplib2.Http()
390
391 http.force_exception_to_status_code = True
392
393 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
394 hostname=rietveld_host,
395 issue=changelist.GetIssue(),
396 patch=options.patchset)
397 params = {'tag': 'buildset:%s' % buildset}
398
399 builds = {}
400 while True:
401 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
402 hostname=options.buildbucket_host,
403 params=urllib.urlencode(params))
404 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
405 for build in content.get('builds', []):
406 builds[build['id']] = build
407 if 'next_cursor' in content:
408 params['start_cursor'] = content['next_cursor']
409 else:
410 break
411 return builds
412
413
414def print_tryjobs(options, builds):
415 """Prints nicely result of fetch_try_jobs."""
416 if not builds:
vapiera7fbd5a2016-06-16 09:17:49 -0700417 print('No tryjobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000418 return
419
420 # Make a copy, because we'll be modifying builds dictionary.
421 builds = builds.copy()
422 builder_names_cache = {}
423
424 def get_builder(b):
425 try:
426 return builder_names_cache[b['id']]
427 except KeyError:
428 try:
429 parameters = json.loads(b['parameters_json'])
430 name = parameters['builder_name']
431 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700432 print('WARNING: failed to get builder name for build %s: %s' % (
433 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000434 name = None
435 builder_names_cache[b['id']] = name
436 return name
437
438 def get_bucket(b):
439 bucket = b['bucket']
440 if bucket.startswith('master.'):
441 return bucket[len('master.'):]
442 return bucket
443
444 if options.print_master:
445 name_fmt = '%%-%ds %%-%ds' % (
446 max(len(str(get_bucket(b))) for b in builds.itervalues()),
447 max(len(str(get_builder(b))) for b in builds.itervalues()))
448 def get_name(b):
449 return name_fmt % (get_bucket(b), get_builder(b))
450 else:
451 name_fmt = '%%-%ds' % (
452 max(len(str(get_builder(b))) for b in builds.itervalues()))
453 def get_name(b):
454 return name_fmt % get_builder(b)
455
456 def sort_key(b):
457 return b['status'], b.get('result'), get_name(b), b.get('url')
458
459 def pop(title, f, color=None, **kwargs):
460 """Pop matching builds from `builds` dict and print them."""
461
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000462 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000463 colorize = str
464 else:
465 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
466
467 result = []
468 for b in builds.values():
469 if all(b.get(k) == v for k, v in kwargs.iteritems()):
470 builds.pop(b['id'])
471 result.append(b)
472 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700473 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000474 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700475 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000476
477 total = len(builds)
478 pop(status='COMPLETED', result='SUCCESS',
479 title='Successes:', color=Fore.GREEN,
480 f=lambda b: (get_name(b), b.get('url')))
481 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
482 title='Infra Failures:', color=Fore.MAGENTA,
483 f=lambda b: (get_name(b), b.get('url')))
484 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
485 title='Failures:', color=Fore.RED,
486 f=lambda b: (get_name(b), b.get('url')))
487 pop(status='COMPLETED', result='CANCELED',
488 title='Canceled:', color=Fore.MAGENTA,
489 f=lambda b: (get_name(b),))
490 pop(status='COMPLETED', result='FAILURE',
491 failure_reason='INVALID_BUILD_DEFINITION',
492 title='Wrong master/builder name:', color=Fore.MAGENTA,
493 f=lambda b: (get_name(b),))
494 pop(status='COMPLETED', result='FAILURE',
495 title='Other failures:',
496 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
497 pop(status='COMPLETED',
498 title='Other finished:',
499 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
500 pop(status='STARTED',
501 title='Started:', color=Fore.YELLOW,
502 f=lambda b: (get_name(b), b.get('url')))
503 pop(status='SCHEDULED',
504 title='Scheduled:',
505 f=lambda b: (get_name(b), 'id=%s' % b['id']))
506 # The last section is just in case buildbucket API changes OR there is a bug.
507 pop(title='Other:',
508 f=lambda b: (get_name(b), 'id=%s' % b['id']))
509 assert len(builds) == 0
vapiera7fbd5a2016-06-16 09:17:49 -0700510 print('Total: %d tryjobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000511
512
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000513def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
514 """Return the corresponding git ref if |base_url| together with |glob_spec|
515 matches the full |url|.
516
517 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
518 """
519 fetch_suburl, as_ref = glob_spec.split(':')
520 if allow_wildcards:
521 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
522 if glob_match:
523 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
524 # "branches/{472,597,648}/src:refs/remotes/svn/*".
525 branch_re = re.escape(base_url)
526 if glob_match.group(1):
527 branch_re += '/' + re.escape(glob_match.group(1))
528 wildcard = glob_match.group(2)
529 if wildcard == '*':
530 branch_re += '([^/]*)'
531 else:
532 # Escape and replace surrounding braces with parentheses and commas
533 # with pipe symbols.
534 wildcard = re.escape(wildcard)
535 wildcard = re.sub('^\\\\{', '(', wildcard)
536 wildcard = re.sub('\\\\,', '|', wildcard)
537 wildcard = re.sub('\\\\}$', ')', wildcard)
538 branch_re += wildcard
539 if glob_match.group(3):
540 branch_re += re.escape(glob_match.group(3))
541 match = re.match(branch_re, url)
542 if match:
543 return re.sub('\*$', match.group(1), as_ref)
544
545 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
546 if fetch_suburl:
547 full_url = base_url + '/' + fetch_suburl
548 else:
549 full_url = base_url
550 if full_url == url:
551 return as_ref
552 return None
553
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000554
iannucci@chromium.org79540052012-10-19 23:15:26 +0000555def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000556 """Prints statistics about the change to the user."""
557 # --no-ext-diff is broken in some versions of Git, so try to work around
558 # this by overriding the environment (but there is still a problem if the
559 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000560 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000561 if 'GIT_EXTERNAL_DIFF' in env:
562 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000563
564 if find_copies:
565 similarity_options = ['--find-copies-harder', '-l100000',
566 '-C%s' % similarity]
567 else:
568 similarity_options = ['-M%s' % similarity]
569
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000570 try:
571 stdout = sys.stdout.fileno()
572 except AttributeError:
573 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000574 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000575 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000576 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000577 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000578
579
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000580class BuildbucketResponseException(Exception):
581 pass
582
583
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000584class Settings(object):
585 def __init__(self):
586 self.default_server = None
587 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000588 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000589 self.is_git_svn = None
590 self.svn_branch = None
591 self.tree_status_url = None
592 self.viewvc_url = None
593 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000594 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000595 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000596 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000597 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000598 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000599 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000600 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000601
602 def LazyUpdateIfNeeded(self):
603 """Updates the settings from a codereview.settings file, if available."""
604 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000605 # The only value that actually changes the behavior is
606 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000607 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000608 error_ok=True
609 ).strip().lower()
610
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000611 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000612 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000613 LoadCodereviewSettingsFromFile(cr_settings_file)
614 self.updated = True
615
616 def GetDefaultServerUrl(self, error_ok=False):
617 if not self.default_server:
618 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000619 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000620 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000621 if error_ok:
622 return self.default_server
623 if not self.default_server:
624 error_message = ('Could not find settings file. You must configure '
625 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000626 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000627 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000628 return self.default_server
629
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000630 @staticmethod
631 def GetRelativeRoot():
632 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000633
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000634 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000635 if self.root is None:
636 self.root = os.path.abspath(self.GetRelativeRoot())
637 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000638
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000639 def GetGitMirror(self, remote='origin'):
640 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000641 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000642 if not os.path.isdir(local_url):
643 return None
644 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
645 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
646 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
647 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
648 if mirror.exists():
649 return mirror
650 return None
651
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000652 def GetIsGitSvn(self):
653 """Return true if this repo looks like it's using git-svn."""
654 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000655 if self.GetPendingRefPrefix():
656 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
657 self.is_git_svn = False
658 else:
659 # If you have any "svn-remote.*" config keys, we think you're using svn.
660 self.is_git_svn = RunGitWithCode(
661 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000662 return self.is_git_svn
663
664 def GetSVNBranch(self):
665 if self.svn_branch is None:
666 if not self.GetIsGitSvn():
667 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
668
669 # Try to figure out which remote branch we're based on.
670 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000671 # 1) iterate through our branch history and find the svn URL.
672 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000673
674 # regexp matching the git-svn line that contains the URL.
675 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
676
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000677 # We don't want to go through all of history, so read a line from the
678 # pipe at a time.
679 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000680 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000681 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
682 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000683 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000684 for line in proc.stdout:
685 match = git_svn_re.match(line)
686 if match:
687 url = match.group(1)
688 proc.stdout.close() # Cut pipe.
689 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000690
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000691 if url:
692 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
693 remotes = RunGit(['config', '--get-regexp',
694 r'^svn-remote\..*\.url']).splitlines()
695 for remote in remotes:
696 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000697 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000698 remote = match.group(1)
699 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000700 rewrite_root = RunGit(
701 ['config', 'svn-remote.%s.rewriteRoot' % remote],
702 error_ok=True).strip()
703 if rewrite_root:
704 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000705 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000706 ['config', 'svn-remote.%s.fetch' % remote],
707 error_ok=True).strip()
708 if fetch_spec:
709 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
710 if self.svn_branch:
711 break
712 branch_spec = RunGit(
713 ['config', 'svn-remote.%s.branches' % remote],
714 error_ok=True).strip()
715 if branch_spec:
716 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
717 if self.svn_branch:
718 break
719 tag_spec = RunGit(
720 ['config', 'svn-remote.%s.tags' % remote],
721 error_ok=True).strip()
722 if tag_spec:
723 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
724 if self.svn_branch:
725 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000726
727 if not self.svn_branch:
728 DieWithError('Can\'t guess svn branch -- try specifying it on the '
729 'command line')
730
731 return self.svn_branch
732
733 def GetTreeStatusUrl(self, error_ok=False):
734 if not self.tree_status_url:
735 error_message = ('You must configure your tree status URL by running '
736 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000737 self.tree_status_url = self._GetRietveldConfig(
738 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000739 return self.tree_status_url
740
741 def GetViewVCUrl(self):
742 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000743 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000744 return self.viewvc_url
745
rmistry@google.com90752582014-01-14 21:04:50 +0000746 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000747 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000748
rmistry@google.com78948ed2015-07-08 23:09:57 +0000749 def GetIsSkipDependencyUpload(self, branch_name):
750 """Returns true if specified branch should skip dep uploads."""
751 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
752 error_ok=True)
753
rmistry@google.com5626a922015-02-26 14:03:30 +0000754 def GetRunPostUploadHook(self):
755 run_post_upload_hook = self._GetRietveldConfig(
756 'run-post-upload-hook', error_ok=True)
757 return run_post_upload_hook == "True"
758
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000759 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000760 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000761
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000762 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000763 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000764
ukai@chromium.orge8077812012-02-03 03:41:46 +0000765 def GetIsGerrit(self):
766 """Return true if this repo is assosiated with gerrit code review system."""
767 if self.is_gerrit is None:
768 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
769 return self.is_gerrit
770
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000771 def GetSquashGerritUploads(self):
772 """Return true if uploads to Gerrit should be squashed by default."""
773 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700774 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
775 if self.squash_gerrit_uploads is None:
776 # Default is squash now (http://crbug.com/611892#c23).
777 self.squash_gerrit_uploads = not (
778 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
779 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000780 return self.squash_gerrit_uploads
781
tandriia60502f2016-06-20 02:01:53 -0700782 def GetSquashGerritUploadsOverride(self):
783 """Return True or False if codereview.settings should be overridden.
784
785 Returns None if no override has been defined.
786 """
787 # See also http://crbug.com/611892#c23
788 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
789 error_ok=True).strip()
790 if result == 'true':
791 return True
792 if result == 'false':
793 return False
794 return None
795
tandrii@chromium.org28253532016-04-14 13:46:56 +0000796 def GetGerritSkipEnsureAuthenticated(self):
797 """Return True if EnsureAuthenticated should not be done for Gerrit
798 uploads."""
799 if self.gerrit_skip_ensure_authenticated is None:
800 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000801 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000802 error_ok=True).strip() == 'true')
803 return self.gerrit_skip_ensure_authenticated
804
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000805 def GetGitEditor(self):
806 """Return the editor specified in the git config, or None if none is."""
807 if self.git_editor is None:
808 self.git_editor = self._GetConfig('core.editor', error_ok=True)
809 return self.git_editor or None
810
thestig@chromium.org44202a22014-03-11 19:22:18 +0000811 def GetLintRegex(self):
812 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
813 DEFAULT_LINT_REGEX)
814
815 def GetLintIgnoreRegex(self):
816 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
817 DEFAULT_LINT_IGNORE_REGEX)
818
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000819 def GetProject(self):
820 if not self.project:
821 self.project = self._GetRietveldConfig('project', error_ok=True)
822 return self.project
823
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000824 def GetForceHttpsCommitUrl(self):
825 if not self.force_https_commit_url:
826 self.force_https_commit_url = self._GetRietveldConfig(
827 'force-https-commit-url', error_ok=True)
828 return self.force_https_commit_url
829
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000830 def GetPendingRefPrefix(self):
831 if not self.pending_ref_prefix:
832 self.pending_ref_prefix = self._GetRietveldConfig(
833 'pending-ref-prefix', error_ok=True)
834 return self.pending_ref_prefix
835
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000836 def _GetRietveldConfig(self, param, **kwargs):
837 return self._GetConfig('rietveld.' + param, **kwargs)
838
rmistry@google.com78948ed2015-07-08 23:09:57 +0000839 def _GetBranchConfig(self, branch_name, param, **kwargs):
840 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
841
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000842 def _GetConfig(self, param, **kwargs):
843 self.LazyUpdateIfNeeded()
844 return RunGit(['config', param], **kwargs).strip()
845
846
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000847def ShortBranchName(branch):
848 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000849 return branch.replace('refs/heads/', '', 1)
850
851
852def GetCurrentBranchRef():
853 """Returns branch ref (e.g., refs/heads/master) or None."""
854 return RunGit(['symbolic-ref', 'HEAD'],
855 stderr=subprocess2.VOID, error_ok=True).strip() or None
856
857
858def GetCurrentBranch():
859 """Returns current branch or None.
860
861 For refs/heads/* branches, returns just last part. For others, full ref.
862 """
863 branchref = GetCurrentBranchRef()
864 if branchref:
865 return ShortBranchName(branchref)
866 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000867
868
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000869class _CQState(object):
870 """Enum for states of CL with respect to Commit Queue."""
871 NONE = 'none'
872 DRY_RUN = 'dry_run'
873 COMMIT = 'commit'
874
875 ALL_STATES = [NONE, DRY_RUN, COMMIT]
876
877
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000878class _ParsedIssueNumberArgument(object):
879 def __init__(self, issue=None, patchset=None, hostname=None):
880 self.issue = issue
881 self.patchset = patchset
882 self.hostname = hostname
883
884 @property
885 def valid(self):
886 return self.issue is not None
887
888
889class _RietveldParsedIssueNumberArgument(_ParsedIssueNumberArgument):
890 def __init__(self, *args, **kwargs):
891 self.patch_url = kwargs.pop('patch_url', None)
892 super(_RietveldParsedIssueNumberArgument, self).__init__(*args, **kwargs)
893
894
895def ParseIssueNumberArgument(arg):
896 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
897 fail_result = _ParsedIssueNumberArgument()
898
899 if arg.isdigit():
900 return _ParsedIssueNumberArgument(issue=int(arg))
901 if not arg.startswith('http'):
902 return fail_result
903 url = gclient_utils.UpgradeToHttps(arg)
904 try:
905 parsed_url = urlparse.urlparse(url)
906 except ValueError:
907 return fail_result
908 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
909 tmp = cls.ParseIssueURL(parsed_url)
910 if tmp is not None:
911 return tmp
912 return fail_result
913
914
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000915class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000916 """Changelist works with one changelist in local branch.
917
918 Supports two codereview backends: Rietveld or Gerrit, selected at object
919 creation.
920
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +0000921 Notes:
922 * Not safe for concurrent multi-{thread,process} use.
923 * Caches values from current branch. Therefore, re-use after branch change
924 with care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000925 """
926
927 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
928 """Create a new ChangeList instance.
929
930 If issue is given, the codereview must be given too.
931
932 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
933 Otherwise, it's decided based on current configuration of the local branch,
934 with default being 'rietveld' for backwards compatibility.
935 See _load_codereview_impl for more details.
936
937 **kwargs will be passed directly to codereview implementation.
938 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000939 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000940 global settings
941 if not settings:
942 # Happens when git_cl.py is used as a utility library.
943 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000944
945 if issue:
946 assert codereview, 'codereview must be known, if issue is known'
947
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000948 self.branchref = branchref
949 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000950 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000951 self.branch = ShortBranchName(self.branchref)
952 else:
953 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000954 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000955 self.lookedup_issue = False
956 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000957 self.has_description = False
958 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000959 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000960 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000961 self.cc = None
962 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000963 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000964
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000965 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000966 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000967 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000968 assert self._codereview_impl
969 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000970
971 def _load_codereview_impl(self, codereview=None, **kwargs):
972 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000973 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
974 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
975 self._codereview = codereview
976 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000977 return
978
979 # Automatic selection based on issue number set for a current branch.
980 # Rietveld takes precedence over Gerrit.
981 assert not self.issue
982 # Whether we find issue or not, we are doing the lookup.
983 self.lookedup_issue = True
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000984 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000985 setting = cls.IssueSetting(self.GetBranch())
986 issue = RunGit(['config', setting], error_ok=True).strip()
987 if issue:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000988 self._codereview = codereview
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000989 self._codereview_impl = cls(self, **kwargs)
990 self.issue = int(issue)
991 return
992
993 # No issue is set for this branch, so decide based on repo-wide settings.
994 return self._load_codereview_impl(
995 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
996 **kwargs)
997
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000998 def IsGerrit(self):
999 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001000
1001 def GetCCList(self):
1002 """Return the users cc'd on this CL.
1003
1004 Return is a string suitable for passing to gcl with the --cc flag.
1005 """
1006 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001007 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001008 more_cc = ','.join(self.watchers)
1009 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1010 return self.cc
1011
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001012 def GetCCListWithoutDefault(self):
1013 """Return the users cc'd on this CL excluding default ones."""
1014 if self.cc is None:
1015 self.cc = ','.join(self.watchers)
1016 return self.cc
1017
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001018 def SetWatchers(self, watchers):
1019 """Set the list of email addresses that should be cc'd based on the changed
1020 files in this CL.
1021 """
1022 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001023
1024 def GetBranch(self):
1025 """Returns the short branch name, e.g. 'master'."""
1026 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001027 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001028 if not branchref:
1029 return None
1030 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001031 self.branch = ShortBranchName(self.branchref)
1032 return self.branch
1033
1034 def GetBranchRef(self):
1035 """Returns the full branch name, e.g. 'refs/heads/master'."""
1036 self.GetBranch() # Poke the lazy loader.
1037 return self.branchref
1038
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001039 def ClearBranch(self):
1040 """Clears cached branch data of this object."""
1041 self.branch = self.branchref = None
1042
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001043 @staticmethod
1044 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001045 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001046 e.g. 'origin', 'refs/heads/master'
1047 """
1048 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001049 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
1050 error_ok=True).strip()
1051 if upstream_branch:
1052 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
1053 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001054 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1055 error_ok=True).strip()
1056 if upstream_branch:
1057 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001058 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001059 # Fall back on trying a git-svn upstream branch.
1060 if settings.GetIsGitSvn():
1061 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001062 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001063 # Else, try to guess the origin remote.
1064 remote_branches = RunGit(['branch', '-r']).split()
1065 if 'origin/master' in remote_branches:
1066 # Fall back on origin/master if it exits.
1067 remote = 'origin'
1068 upstream_branch = 'refs/heads/master'
1069 elif 'origin/trunk' in remote_branches:
1070 # Fall back on origin/trunk if it exists. Generally a shared
1071 # git-svn clone
1072 remote = 'origin'
1073 upstream_branch = 'refs/heads/trunk'
1074 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001075 DieWithError(
1076 'Unable to determine default branch to diff against.\n'
1077 'Either pass complete "git diff"-style arguments, like\n'
1078 ' git cl upload origin/master\n'
1079 'or verify this branch is set up to track another \n'
1080 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001081
1082 return remote, upstream_branch
1083
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001084 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001085 upstream_branch = self.GetUpstreamBranch()
1086 if not BranchExists(upstream_branch):
1087 DieWithError('The upstream for the current branch (%s) does not exist '
1088 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001089 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001090 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001091
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001092 def GetUpstreamBranch(self):
1093 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001094 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001095 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001096 upstream_branch = upstream_branch.replace('refs/heads/',
1097 'refs/remotes/%s/' % remote)
1098 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1099 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001100 self.upstream_branch = upstream_branch
1101 return self.upstream_branch
1102
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001103 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001104 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001105 remote, branch = None, self.GetBranch()
1106 seen_branches = set()
1107 while branch not in seen_branches:
1108 seen_branches.add(branch)
1109 remote, branch = self.FetchUpstreamTuple(branch)
1110 branch = ShortBranchName(branch)
1111 if remote != '.' or branch.startswith('refs/remotes'):
1112 break
1113 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001114 remotes = RunGit(['remote'], error_ok=True).split()
1115 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001116 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001117 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001118 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001119 logging.warning('Could not determine which remote this change is '
1120 'associated with, so defaulting to "%s". This may '
1121 'not be what you want. You may prevent this message '
1122 'by running "git svn info" as documented here: %s',
1123 self._remote,
1124 GIT_INSTRUCTIONS_URL)
1125 else:
1126 logging.warn('Could not determine which remote this change is '
1127 'associated with. You may prevent this message by '
1128 'running "git svn info" as documented here: %s',
1129 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001130 branch = 'HEAD'
1131 if branch.startswith('refs/remotes'):
1132 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001133 elif branch.startswith('refs/branch-heads/'):
1134 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001135 else:
1136 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001137 return self._remote
1138
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001139 def GitSanityChecks(self, upstream_git_obj):
1140 """Checks git repo status and ensures diff is from local commits."""
1141
sbc@chromium.org79706062015-01-14 21:18:12 +00001142 if upstream_git_obj is None:
1143 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001144 print('ERROR: unable to determine current branch (detached HEAD?)',
1145 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001146 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001147 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001148 return False
1149
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001150 # Verify the commit we're diffing against is in our current branch.
1151 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1152 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1153 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001154 print('ERROR: %s is not in the current branch. You may need to rebase '
1155 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001156 return False
1157
1158 # List the commits inside the diff, and verify they are all local.
1159 commits_in_diff = RunGit(
1160 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1161 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1162 remote_branch = remote_branch.strip()
1163 if code != 0:
1164 _, remote_branch = self.GetRemoteBranch()
1165
1166 commits_in_remote = RunGit(
1167 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1168
1169 common_commits = set(commits_in_diff) & set(commits_in_remote)
1170 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001171 print('ERROR: Your diff contains %d commits already in %s.\n'
1172 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1173 'the diff. If you are using a custom git flow, you can override'
1174 ' the reference used for this check with "git config '
1175 'gitcl.remotebranch <git-ref>".' % (
1176 len(common_commits), remote_branch, upstream_git_obj),
1177 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001178 return False
1179 return True
1180
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001181 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001182 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001183
1184 Returns None if it is not set.
1185 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001186 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
1187 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001188
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001189 def GetGitSvnRemoteUrl(self):
1190 """Return the configured git-svn remote URL parsed from git svn info.
1191
1192 Returns None if it is not set.
1193 """
1194 # URL is dependent on the current directory.
1195 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1196 if data:
1197 keys = dict(line.split(': ', 1) for line in data.splitlines()
1198 if ': ' in line)
1199 return keys.get('URL', None)
1200 return None
1201
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001202 def GetRemoteUrl(self):
1203 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1204
1205 Returns None if there is no remote.
1206 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001207 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001208 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1209
1210 # If URL is pointing to a local directory, it is probably a git cache.
1211 if os.path.isdir(url):
1212 url = RunGit(['config', 'remote.%s.url' % remote],
1213 error_ok=True,
1214 cwd=url).strip()
1215 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001216
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001217 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001218 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001219 if self.issue is None and not self.lookedup_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001220 issue = RunGit(['config',
1221 self._codereview_impl.IssueSetting(self.GetBranch())],
1222 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001223 self.issue = int(issue) or None if issue else None
1224 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001225 return self.issue
1226
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001227 def GetIssueURL(self):
1228 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001229 issue = self.GetIssue()
1230 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001231 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001232 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001233
1234 def GetDescription(self, pretty=False):
1235 if not self.has_description:
1236 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001237 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001238 self.has_description = True
1239 if pretty:
1240 wrapper = textwrap.TextWrapper()
1241 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1242 return wrapper.fill(self.description)
1243 return self.description
1244
1245 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001246 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001247 if self.patchset is None and not self.lookedup_patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001248 patchset = RunGit(['config', self._codereview_impl.PatchsetSetting()],
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001249 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001250 self.patchset = int(patchset) or None if patchset else None
1251 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001252 return self.patchset
1253
1254 def SetPatchset(self, patchset):
1255 """Set this branch's patchset. If patchset=0, clears the patchset."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001256 patchset_setting = self._codereview_impl.PatchsetSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001257 if patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001258 RunGit(['config', patchset_setting, str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001259 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001260 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001261 RunGit(['config', '--unset', patchset_setting],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001262 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001263 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001264
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001265 def SetIssue(self, issue=None):
1266 """Set this branch's issue. If issue isn't given, clears the issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001267 issue_setting = self._codereview_impl.IssueSetting(self.GetBranch())
1268 codereview_setting = self._codereview_impl.GetCodereviewServerSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001269 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001270 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001271 RunGit(['config', issue_setting, str(issue)])
1272 codereview_server = self._codereview_impl.GetCodereviewServer()
1273 if codereview_server:
1274 RunGit(['config', codereview_setting, codereview_server])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001275 else:
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001276 # Reset it regardless. It doesn't hurt.
1277 config_settings = [issue_setting, self._codereview_impl.PatchsetSetting()]
1278 for prop in (['last-upload-hash'] +
1279 self._codereview_impl._PostUnsetIssueProperties()):
1280 config_settings.append('branch.%s.%s' % (self.GetBranch(), prop))
1281 for setting in config_settings:
1282 RunGit(['config', '--unset', setting], error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001283 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001284 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001285
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001286 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001287 if not self.GitSanityChecks(upstream_branch):
1288 DieWithError('\nGit sanity check failure')
1289
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001290 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001291 if not root:
1292 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001293 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001294
1295 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001296 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001297 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001298 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001299 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001300 except subprocess2.CalledProcessError:
1301 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001302 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001303 'This branch probably doesn\'t exist anymore. To reset the\n'
1304 'tracking branch, please run\n'
1305 ' git branch --set-upstream %s trunk\n'
1306 'replacing trunk with origin/master or the relevant branch') %
1307 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001308
maruel@chromium.org52424302012-08-29 15:14:30 +00001309 issue = self.GetIssue()
1310 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001311 if issue:
1312 description = self.GetDescription()
1313 else:
1314 # If the change was never uploaded, use the log messages of all commits
1315 # up to the branch point, as git cl upload will prefill the description
1316 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001317 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1318 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001319
1320 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001321 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001322 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001323 name,
1324 description,
1325 absroot,
1326 files,
1327 issue,
1328 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001329 author,
1330 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001331
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001332 def UpdateDescription(self, description):
1333 self.description = description
1334 return self._codereview_impl.UpdateDescriptionRemote(description)
1335
1336 def RunHook(self, committing, may_prompt, verbose, change):
1337 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1338 try:
1339 return presubmit_support.DoPresubmitChecks(change, committing,
1340 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1341 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001342 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1343 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001344 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001345 DieWithError(
1346 ('%s\nMaybe your depot_tools is out of date?\n'
1347 'If all fails, contact maruel@') % e)
1348
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001349 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1350 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001351 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1352 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001353 else:
1354 # Assume url.
1355 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1356 urlparse.urlparse(issue_arg))
1357 if not parsed_issue_arg or not parsed_issue_arg.valid:
1358 DieWithError('Failed to parse issue argument "%s". '
1359 'Must be an issue number or a valid URL.' % issue_arg)
1360 return self._codereview_impl.CMDPatchWithParsedIssue(
1361 parsed_issue_arg, reject, nocommit, directory)
1362
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001363 def CMDUpload(self, options, git_diff_args, orig_args):
1364 """Uploads a change to codereview."""
1365 if git_diff_args:
1366 # TODO(ukai): is it ok for gerrit case?
1367 base_branch = git_diff_args[0]
1368 else:
1369 if self.GetBranch() is None:
1370 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1371
1372 # Default to diffing against common ancestor of upstream branch
1373 base_branch = self.GetCommonAncestorWithUpstream()
1374 git_diff_args = [base_branch, 'HEAD']
1375
1376 # Make sure authenticated to codereview before running potentially expensive
1377 # hooks. It is a fast, best efforts check. Codereview still can reject the
1378 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001379 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001380
1381 # Apply watchlists on upload.
1382 change = self.GetChange(base_branch, None)
1383 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1384 files = [f.LocalPath() for f in change.AffectedFiles()]
1385 if not options.bypass_watchlists:
1386 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1387
1388 if not options.bypass_hooks:
1389 if options.reviewers or options.tbr_owners:
1390 # Set the reviewer list now so that presubmit checks can access it.
1391 change_description = ChangeDescription(change.FullDescriptionText())
1392 change_description.update_reviewers(options.reviewers,
1393 options.tbr_owners,
1394 change)
1395 change.SetDescriptionText(change_description.description)
1396 hook_results = self.RunHook(committing=False,
1397 may_prompt=not options.force,
1398 verbose=options.verbose,
1399 change=change)
1400 if not hook_results.should_continue():
1401 return 1
1402 if not options.reviewers and hook_results.reviewers:
1403 options.reviewers = hook_results.reviewers.split(',')
1404
1405 if self.GetIssue():
1406 latest_patchset = self.GetMostRecentPatchset()
1407 local_patchset = self.GetPatchset()
1408 if (latest_patchset and local_patchset and
1409 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001410 print('The last upload made from this repository was patchset #%d but '
1411 'the most recent patchset on the server is #%d.'
1412 % (local_patchset, latest_patchset))
1413 print('Uploading will still work, but if you\'ve uploaded to this '
1414 'issue from another machine or branch the patch you\'re '
1415 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001416 ask_for_data('About to upload; enter to confirm.')
1417
1418 print_stats(options.similarity, options.find_copies, git_diff_args)
1419 ret = self.CMDUploadChange(options, git_diff_args, change)
1420 if not ret:
1421 git_set_branch_value('last-upload-hash',
1422 RunGit(['rev-parse', 'HEAD']).strip())
1423 # Run post upload hooks, if specified.
1424 if settings.GetRunPostUploadHook():
1425 presubmit_support.DoPostUploadExecuter(
1426 change,
1427 self,
1428 settings.GetRoot(),
1429 options.verbose,
1430 sys.stdout)
1431
1432 # Upload all dependencies if specified.
1433 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001434 print()
1435 print('--dependencies has been specified.')
1436 print('All dependent local branches will be re-uploaded.')
1437 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001438 # Remove the dependencies flag from args so that we do not end up in a
1439 # loop.
1440 orig_args.remove('--dependencies')
1441 ret = upload_branch_deps(self, orig_args)
1442 return ret
1443
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001444 def SetCQState(self, new_state):
1445 """Update the CQ state for latest patchset.
1446
1447 Issue must have been already uploaded and known.
1448 """
1449 assert new_state in _CQState.ALL_STATES
1450 assert self.GetIssue()
1451 return self._codereview_impl.SetCQState(new_state)
1452
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001453 # Forward methods to codereview specific implementation.
1454
1455 def CloseIssue(self):
1456 return self._codereview_impl.CloseIssue()
1457
1458 def GetStatus(self):
1459 return self._codereview_impl.GetStatus()
1460
1461 def GetCodereviewServer(self):
1462 return self._codereview_impl.GetCodereviewServer()
1463
1464 def GetApprovingReviewers(self):
1465 return self._codereview_impl.GetApprovingReviewers()
1466
1467 def GetMostRecentPatchset(self):
1468 return self._codereview_impl.GetMostRecentPatchset()
1469
1470 def __getattr__(self, attr):
1471 # This is because lots of untested code accesses Rietveld-specific stuff
1472 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001473 # on a case by case basis.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001474 return getattr(self._codereview_impl, attr)
1475
1476
1477class _ChangelistCodereviewBase(object):
1478 """Abstract base class encapsulating codereview specifics of a changelist."""
1479 def __init__(self, changelist):
1480 self._changelist = changelist # instance of Changelist
1481
1482 def __getattr__(self, attr):
1483 # Forward methods to changelist.
1484 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1485 # _RietveldChangelistImpl to avoid this hack?
1486 return getattr(self._changelist, attr)
1487
1488 def GetStatus(self):
1489 """Apply a rough heuristic to give a simple summary of an issue's review
1490 or CQ status, assuming adherence to a common workflow.
1491
1492 Returns None if no issue for this branch, or specific string keywords.
1493 """
1494 raise NotImplementedError()
1495
1496 def GetCodereviewServer(self):
1497 """Returns server URL without end slash, like "https://codereview.com"."""
1498 raise NotImplementedError()
1499
1500 def FetchDescription(self):
1501 """Fetches and returns description from the codereview server."""
1502 raise NotImplementedError()
1503
1504 def GetCodereviewServerSetting(self):
1505 """Returns git config setting for the codereview server."""
1506 raise NotImplementedError()
1507
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001508 @classmethod
1509 def IssueSetting(cls, branch):
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001510 return 'branch.%s.%s' % (branch, cls.IssueSettingSuffix())
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001511
1512 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001513 def IssueSettingSuffix(cls):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001514 """Returns name of git config setting which stores issue number for a given
1515 branch."""
1516 raise NotImplementedError()
1517
1518 def PatchsetSetting(self):
1519 """Returns name of git config setting which stores issue number."""
1520 raise NotImplementedError()
1521
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001522 def _PostUnsetIssueProperties(self):
1523 """Which branch-specific properties to erase when unsettin issue."""
1524 raise NotImplementedError()
1525
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001526 def GetRieveldObjForPresubmit(self):
1527 # This is an unfortunate Rietveld-embeddedness in presubmit.
1528 # For non-Rietveld codereviews, this probably should return a dummy object.
1529 raise NotImplementedError()
1530
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001531 def GetGerritObjForPresubmit(self):
1532 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1533 return None
1534
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001535 def UpdateDescriptionRemote(self, description):
1536 """Update the description on codereview site."""
1537 raise NotImplementedError()
1538
1539 def CloseIssue(self):
1540 """Closes the issue."""
1541 raise NotImplementedError()
1542
1543 def GetApprovingReviewers(self):
1544 """Returns a list of reviewers approving the change.
1545
1546 Note: not necessarily committers.
1547 """
1548 raise NotImplementedError()
1549
1550 def GetMostRecentPatchset(self):
1551 """Returns the most recent patchset number from the codereview site."""
1552 raise NotImplementedError()
1553
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001554 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1555 directory):
1556 """Fetches and applies the issue.
1557
1558 Arguments:
1559 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1560 reject: if True, reject the failed patch instead of switching to 3-way
1561 merge. Rietveld only.
1562 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1563 only.
1564 directory: switch to directory before applying the patch. Rietveld only.
1565 """
1566 raise NotImplementedError()
1567
1568 @staticmethod
1569 def ParseIssueURL(parsed_url):
1570 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1571 failed."""
1572 raise NotImplementedError()
1573
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001574 def EnsureAuthenticated(self, force):
1575 """Best effort check that user is authenticated with codereview server.
1576
1577 Arguments:
1578 force: whether to skip confirmation questions.
1579 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001580 raise NotImplementedError()
1581
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001582 def CMDUploadChange(self, options, args, change):
1583 """Uploads a change to codereview."""
1584 raise NotImplementedError()
1585
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001586 def SetCQState(self, new_state):
1587 """Update the CQ state for latest patchset.
1588
1589 Issue must have been already uploaded and known.
1590 """
1591 raise NotImplementedError()
1592
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001593
1594class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1595 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1596 super(_RietveldChangelistImpl, self).__init__(changelist)
1597 assert settings, 'must be initialized in _ChangelistCodereviewBase'
1598 settings.GetDefaultServerUrl()
1599
1600 self._rietveld_server = rietveld_server
1601 self._auth_config = auth_config
1602 self._props = None
1603 self._rpc_server = None
1604
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001605 def GetCodereviewServer(self):
1606 if not self._rietveld_server:
1607 # If we're on a branch then get the server potentially associated
1608 # with that branch.
1609 if self.GetIssue():
1610 rietveld_server_setting = self.GetCodereviewServerSetting()
1611 if rietveld_server_setting:
1612 self._rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1613 ['config', rietveld_server_setting], error_ok=True).strip())
1614 if not self._rietveld_server:
1615 self._rietveld_server = settings.GetDefaultServerUrl()
1616 return self._rietveld_server
1617
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001618 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001619 """Best effort check that user is authenticated with Rietveld server."""
1620 if self._auth_config.use_oauth2:
1621 authenticator = auth.get_authenticator_for_host(
1622 self.GetCodereviewServer(), self._auth_config)
1623 if not authenticator.has_cached_credentials():
1624 raise auth.LoginRequiredError(self.GetCodereviewServer())
1625
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001626 def FetchDescription(self):
1627 issue = self.GetIssue()
1628 assert issue
1629 try:
1630 return self.RpcServer().get_description(issue).strip()
1631 except urllib2.HTTPError as e:
1632 if e.code == 404:
1633 DieWithError(
1634 ('\nWhile fetching the description for issue %d, received a '
1635 '404 (not found)\n'
1636 'error. It is likely that you deleted this '
1637 'issue on the server. If this is the\n'
1638 'case, please run\n\n'
1639 ' git cl issue 0\n\n'
1640 'to clear the association with the deleted issue. Then run '
1641 'this command again.') % issue)
1642 else:
1643 DieWithError(
1644 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1645 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001646 print('Warning: Failed to retrieve CL description due to network '
1647 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001648 return ''
1649
1650 def GetMostRecentPatchset(self):
1651 return self.GetIssueProperties()['patchsets'][-1]
1652
1653 def GetPatchSetDiff(self, issue, patchset):
1654 return self.RpcServer().get(
1655 '/download/issue%s_%s.diff' % (issue, patchset))
1656
1657 def GetIssueProperties(self):
1658 if self._props is None:
1659 issue = self.GetIssue()
1660 if not issue:
1661 self._props = {}
1662 else:
1663 self._props = self.RpcServer().get_issue_properties(issue, True)
1664 return self._props
1665
1666 def GetApprovingReviewers(self):
1667 return get_approving_reviewers(self.GetIssueProperties())
1668
1669 def AddComment(self, message):
1670 return self.RpcServer().add_comment(self.GetIssue(), message)
1671
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001672 def GetStatus(self):
1673 """Apply a rough heuristic to give a simple summary of an issue's review
1674 or CQ status, assuming adherence to a common workflow.
1675
1676 Returns None if no issue for this branch, or one of the following keywords:
1677 * 'error' - error from review tool (including deleted issues)
1678 * 'unsent' - not sent for review
1679 * 'waiting' - waiting for review
1680 * 'reply' - waiting for owner to reply to review
1681 * 'lgtm' - LGTM from at least one approved reviewer
1682 * 'commit' - in the commit queue
1683 * 'closed' - closed
1684 """
1685 if not self.GetIssue():
1686 return None
1687
1688 try:
1689 props = self.GetIssueProperties()
1690 except urllib2.HTTPError:
1691 return 'error'
1692
1693 if props.get('closed'):
1694 # Issue is closed.
1695 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001696 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001697 # Issue is in the commit queue.
1698 return 'commit'
1699
1700 try:
1701 reviewers = self.GetApprovingReviewers()
1702 except urllib2.HTTPError:
1703 return 'error'
1704
1705 if reviewers:
1706 # Was LGTM'ed.
1707 return 'lgtm'
1708
1709 messages = props.get('messages') or []
1710
tandrii9d2c7a32016-06-22 03:42:45 -07001711 # Skip CQ messages that don't require owner's action.
1712 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1713 if 'Dry run:' in messages[-1]['text']:
1714 messages.pop()
1715 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1716 # This message always follows prior messages from CQ,
1717 # so skip this too.
1718 messages.pop()
1719 else:
1720 # This is probably a CQ messages warranting user attention.
1721 break
1722
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001723 if not messages:
1724 # No message was sent.
1725 return 'unsent'
1726 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07001727 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001728 return 'reply'
1729 return 'waiting'
1730
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001731 def UpdateDescriptionRemote(self, description):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001732 return self.RpcServer().update_description(
1733 self.GetIssue(), self.description)
1734
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001735 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001736 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001737
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001738 def SetFlag(self, flag, value):
1739 """Patchset must match."""
1740 if not self.GetPatchset():
1741 DieWithError('The patchset needs to match. Send another patchset.')
1742 try:
1743 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001744 self.GetIssue(), self.GetPatchset(), flag, value)
vapierfd77ac72016-06-16 08:33:57 -07001745 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001746 if e.code == 404:
1747 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1748 if e.code == 403:
1749 DieWithError(
1750 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1751 'match?') % (self.GetIssue(), self.GetPatchset()))
1752 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001753
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001754 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001755 """Returns an upload.RpcServer() to access this review's rietveld instance.
1756 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001757 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001758 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001759 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001760 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001761 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001762
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001763 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001764 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001765 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001766
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001767 def PatchsetSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001768 """Return the git setting that stores this change's most recent patchset."""
1769 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1770
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001771 def GetCodereviewServerSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001772 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001773 branch = self.GetBranch()
1774 if branch:
1775 return 'branch.%s.rietveldserver' % branch
1776 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001777
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001778 def _PostUnsetIssueProperties(self):
1779 """Which branch-specific properties to erase when unsetting issue."""
1780 return ['rietveldserver']
1781
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001782 def GetRieveldObjForPresubmit(self):
1783 return self.RpcServer()
1784
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001785 def SetCQState(self, new_state):
1786 props = self.GetIssueProperties()
1787 if props.get('private'):
1788 DieWithError('Cannot set-commit on private issue')
1789
1790 if new_state == _CQState.COMMIT:
1791 self.SetFlag('commit', '1')
1792 elif new_state == _CQState.NONE:
1793 self.SetFlag('commit', '0')
1794 else:
1795 raise NotImplementedError()
1796
1797
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001798 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1799 directory):
1800 # TODO(maruel): Use apply_issue.py
1801
1802 # PatchIssue should never be called with a dirty tree. It is up to the
1803 # caller to check this, but just in case we assert here since the
1804 # consequences of the caller not checking this could be dire.
1805 assert(not git_common.is_dirty_git_tree('apply'))
1806 assert(parsed_issue_arg.valid)
1807 self._changelist.issue = parsed_issue_arg.issue
1808 if parsed_issue_arg.hostname:
1809 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1810
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001811 if (isinstance(parsed_issue_arg, _RietveldParsedIssueNumberArgument) and
1812 parsed_issue_arg.patch_url):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001813 assert parsed_issue_arg.patchset
1814 patchset = parsed_issue_arg.patchset
1815 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1816 else:
1817 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1818 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1819
1820 # Switch up to the top-level directory, if necessary, in preparation for
1821 # applying the patch.
1822 top = settings.GetRelativeRoot()
1823 if top:
1824 os.chdir(top)
1825
1826 # Git patches have a/ at the beginning of source paths. We strip that out
1827 # with a sed script rather than the -p flag to patch so we can feed either
1828 # Git or svn-style patches into the same apply command.
1829 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1830 try:
1831 patch_data = subprocess2.check_output(
1832 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1833 except subprocess2.CalledProcessError:
1834 DieWithError('Git patch mungling failed.')
1835 logging.info(patch_data)
1836
1837 # We use "git apply" to apply the patch instead of "patch" so that we can
1838 # pick up file adds.
1839 # The --index flag means: also insert into the index (so we catch adds).
1840 cmd = ['git', 'apply', '--index', '-p0']
1841 if directory:
1842 cmd.extend(('--directory', directory))
1843 if reject:
1844 cmd.append('--reject')
1845 elif IsGitVersionAtLeast('1.7.12'):
1846 cmd.append('--3way')
1847 try:
1848 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
1849 stdin=patch_data, stdout=subprocess2.VOID)
1850 except subprocess2.CalledProcessError:
vapiera7fbd5a2016-06-16 09:17:49 -07001851 print('Failed to apply the patch')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001852 return 1
1853
1854 # If we had an issue, commit the current state and register the issue.
1855 if not nocommit:
1856 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
1857 'patch from issue %(i)s at patchset '
1858 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
1859 % {'i': self.GetIssue(), 'p': patchset})])
1860 self.SetIssue(self.GetIssue())
1861 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07001862 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001863 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001864 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001865 return 0
1866
1867 @staticmethod
1868 def ParseIssueURL(parsed_url):
1869 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1870 return None
1871 # Typical url: https://domain/<issue_number>[/[other]]
1872 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
1873 if match:
1874 return _RietveldParsedIssueNumberArgument(
1875 issue=int(match.group(1)),
1876 hostname=parsed_url.netloc)
1877 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
1878 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
1879 if match:
1880 return _RietveldParsedIssueNumberArgument(
1881 issue=int(match.group(1)),
1882 patchset=int(match.group(2)),
1883 hostname=parsed_url.netloc,
1884 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
1885 return None
1886
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001887 def CMDUploadChange(self, options, args, change):
1888 """Upload the patch to Rietveld."""
1889 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1890 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001891 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
1892 if options.emulate_svn_auto_props:
1893 upload_args.append('--emulate_svn_auto_props')
1894
1895 change_desc = None
1896
1897 if options.email is not None:
1898 upload_args.extend(['--email', options.email])
1899
1900 if self.GetIssue():
1901 if options.title:
1902 upload_args.extend(['--title', options.title])
1903 if options.message:
1904 upload_args.extend(['--message', options.message])
1905 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07001906 print('This branch is associated with issue %s. '
1907 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001908 else:
1909 if options.title:
1910 upload_args.extend(['--title', options.title])
1911 message = (options.title or options.message or
1912 CreateDescriptionFromLog(args))
1913 change_desc = ChangeDescription(message)
1914 if options.reviewers or options.tbr_owners:
1915 change_desc.update_reviewers(options.reviewers,
1916 options.tbr_owners,
1917 change)
1918 if not options.force:
1919 change_desc.prompt()
1920
1921 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07001922 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001923 return 1
1924
1925 upload_args.extend(['--message', change_desc.description])
1926 if change_desc.get_reviewers():
1927 upload_args.append('--reviewers=%s' % ','.join(
1928 change_desc.get_reviewers()))
1929 if options.send_mail:
1930 if not change_desc.get_reviewers():
1931 DieWithError("Must specify reviewers to send email.")
1932 upload_args.append('--send_mail')
1933
1934 # We check this before applying rietveld.private assuming that in
1935 # rietveld.cc only addresses which we can send private CLs to are listed
1936 # if rietveld.private is set, and so we should ignore rietveld.cc only
1937 # when --private is specified explicitly on the command line.
1938 if options.private:
1939 logging.warn('rietveld.cc is ignored since private flag is specified. '
1940 'You need to review and add them manually if necessary.')
1941 cc = self.GetCCListWithoutDefault()
1942 else:
1943 cc = self.GetCCList()
1944 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
1945 if cc:
1946 upload_args.extend(['--cc', cc])
1947
1948 if options.private or settings.GetDefaultPrivateFlag() == "True":
1949 upload_args.append('--private')
1950
1951 upload_args.extend(['--git_similarity', str(options.similarity)])
1952 if not options.find_copies:
1953 upload_args.extend(['--git_no_find_copies'])
1954
1955 # Include the upstream repo's URL in the change -- this is useful for
1956 # projects that have their source spread across multiple repos.
1957 remote_url = self.GetGitBaseUrlFromConfig()
1958 if not remote_url:
1959 if settings.GetIsGitSvn():
1960 remote_url = self.GetGitSvnRemoteUrl()
1961 else:
1962 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
1963 remote_url = '%s@%s' % (self.GetRemoteUrl(),
1964 self.GetUpstreamBranch().split('/')[-1])
1965 if remote_url:
1966 upload_args.extend(['--base_url', remote_url])
1967 remote, remote_branch = self.GetRemoteBranch()
1968 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
1969 settings.GetPendingRefPrefix())
1970 if target_ref:
1971 upload_args.extend(['--target_ref', target_ref])
1972
1973 # Look for dependent patchsets. See crbug.com/480453 for more details.
1974 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
1975 upstream_branch = ShortBranchName(upstream_branch)
1976 if remote is '.':
1977 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001978 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001979 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07001980 print()
1981 print('Skipping dependency patchset upload because git config '
1982 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
1983 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001984 else:
1985 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001986 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001987 auth_config=auth_config)
1988 branch_cl_issue_url = branch_cl.GetIssueURL()
1989 branch_cl_issue = branch_cl.GetIssue()
1990 branch_cl_patchset = branch_cl.GetPatchset()
1991 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
1992 upload_args.extend(
1993 ['--depends_on_patchset', '%s:%s' % (
1994 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001995 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001996 '\n'
1997 'The current branch (%s) is tracking a local branch (%s) with '
1998 'an associated CL.\n'
1999 'Adding %s/#ps%s as a dependency patchset.\n'
2000 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2001 branch_cl_patchset))
2002
2003 project = settings.GetProject()
2004 if project:
2005 upload_args.extend(['--project', project])
2006
2007 if options.cq_dry_run:
2008 upload_args.extend(['--cq_dry_run'])
2009
2010 try:
2011 upload_args = ['upload'] + upload_args + args
2012 logging.info('upload.RealMain(%s)', upload_args)
2013 issue, patchset = upload.RealMain(upload_args)
2014 issue = int(issue)
2015 patchset = int(patchset)
2016 except KeyboardInterrupt:
2017 sys.exit(1)
2018 except:
2019 # If we got an exception after the user typed a description for their
2020 # change, back up the description before re-raising.
2021 if change_desc:
2022 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2023 print('\nGot exception while uploading -- saving description to %s\n' %
2024 backup_path)
2025 backup_file = open(backup_path, 'w')
2026 backup_file.write(change_desc.description)
2027 backup_file.close()
2028 raise
2029
2030 if not self.GetIssue():
2031 self.SetIssue(issue)
2032 self.SetPatchset(patchset)
2033
2034 if options.use_commit_queue:
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002035 self.SetCQState(_CQState.COMMIT)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002036 return 0
2037
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002038
2039class _GerritChangelistImpl(_ChangelistCodereviewBase):
2040 def __init__(self, changelist, auth_config=None):
2041 # auth_config is Rietveld thing, kept here to preserve interface only.
2042 super(_GerritChangelistImpl, self).__init__(changelist)
2043 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002044 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002045 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002046 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002047
2048 def _GetGerritHost(self):
2049 # Lazy load of configs.
2050 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002051 if self._gerrit_host and '.' not in self._gerrit_host:
2052 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2053 # This happens for internal stuff http://crbug.com/614312.
2054 parsed = urlparse.urlparse(self.GetRemoteUrl())
2055 if parsed.scheme == 'sso':
2056 print('WARNING: using non https URLs for remote is likely broken\n'
2057 ' Your current remote is: %s' % self.GetRemoteUrl())
2058 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2059 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002060 return self._gerrit_host
2061
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002062 def _GetGitHost(self):
2063 """Returns git host to be used when uploading change to Gerrit."""
2064 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2065
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002066 def GetCodereviewServer(self):
2067 if not self._gerrit_server:
2068 # If we're on a branch then get the server potentially associated
2069 # with that branch.
2070 if self.GetIssue():
2071 gerrit_server_setting = self.GetCodereviewServerSetting()
2072 if gerrit_server_setting:
2073 self._gerrit_server = RunGit(['config', gerrit_server_setting],
2074 error_ok=True).strip()
2075 if self._gerrit_server:
2076 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
2077 if not self._gerrit_server:
2078 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2079 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002080 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002081 parts[0] = parts[0] + '-review'
2082 self._gerrit_host = '.'.join(parts)
2083 self._gerrit_server = 'https://%s' % self._gerrit_host
2084 return self._gerrit_server
2085
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002086 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00002087 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002088 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002089
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002090 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002091 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002092 if settings.GetGerritSkipEnsureAuthenticated():
2093 # For projects with unusual authentication schemes.
2094 # See http://crbug.com/603378.
2095 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002096 # Lazy-loader to identify Gerrit and Git hosts.
2097 if gerrit_util.GceAuthenticator.is_gce():
2098 return
2099 self.GetCodereviewServer()
2100 git_host = self._GetGitHost()
2101 assert self._gerrit_server and self._gerrit_host
2102 cookie_auth = gerrit_util.CookiesAuthenticator()
2103
2104 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2105 git_auth = cookie_auth.get_auth_header(git_host)
2106 if gerrit_auth and git_auth:
2107 if gerrit_auth == git_auth:
2108 return
2109 print((
2110 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2111 ' Check your %s or %s file for credentials of hosts:\n'
2112 ' %s\n'
2113 ' %s\n'
2114 ' %s') %
2115 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2116 git_host, self._gerrit_host,
2117 cookie_auth.get_new_password_message(git_host)))
2118 if not force:
2119 ask_for_data('If you know what you are doing, press Enter to continue, '
2120 'Ctrl+C to abort.')
2121 return
2122 else:
2123 missing = (
2124 [] if gerrit_auth else [self._gerrit_host] +
2125 [] if git_auth else [git_host])
2126 DieWithError('Credentials for the following hosts are required:\n'
2127 ' %s\n'
2128 'These are read from %s (or legacy %s)\n'
2129 '%s' % (
2130 '\n '.join(missing),
2131 cookie_auth.get_gitcookies_path(),
2132 cookie_auth.get_netrc_path(),
2133 cookie_auth.get_new_password_message(git_host)))
2134
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002135
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002136 def PatchsetSetting(self):
2137 """Return the git setting that stores this change's most recent patchset."""
2138 return 'branch.%s.gerritpatchset' % self.GetBranch()
2139
2140 def GetCodereviewServerSetting(self):
2141 """Returns the git setting that stores this change's Gerrit server."""
2142 branch = self.GetBranch()
2143 if branch:
2144 return 'branch.%s.gerritserver' % branch
2145 return None
2146
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002147 def _PostUnsetIssueProperties(self):
2148 """Which branch-specific properties to erase when unsetting issue."""
2149 return [
2150 'gerritserver',
2151 'gerritsquashhash',
2152 ]
2153
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002154 def GetRieveldObjForPresubmit(self):
2155 class ThisIsNotRietveldIssue(object):
2156 def __nonzero__(self):
2157 # This is a hack to make presubmit_support think that rietveld is not
2158 # defined, yet still ensure that calls directly result in a decent
2159 # exception message below.
2160 return False
2161
2162 def __getattr__(self, attr):
2163 print(
2164 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2165 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2166 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2167 'or use Rietveld for codereview.\n'
2168 'See also http://crbug.com/579160.' % attr)
2169 raise NotImplementedError()
2170 return ThisIsNotRietveldIssue()
2171
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002172 def GetGerritObjForPresubmit(self):
2173 return presubmit_support.GerritAccessor(self._GetGerritHost())
2174
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002175 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002176 """Apply a rough heuristic to give a simple summary of an issue's review
2177 or CQ status, assuming adherence to a common workflow.
2178
2179 Returns None if no issue for this branch, or one of the following keywords:
2180 * 'error' - error from review tool (including deleted issues)
2181 * 'unsent' - no reviewers added
2182 * 'waiting' - waiting for review
2183 * 'reply' - waiting for owner to reply to review
2184 * 'not lgtm' - Code-Review -2 from at least one approved reviewer
2185 * 'lgtm' - Code-Review +2 from at least one approved reviewer
2186 * 'commit' - in the commit queue
2187 * 'closed' - abandoned
2188 """
2189 if not self.GetIssue():
2190 return None
2191
2192 try:
2193 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
2194 except httplib.HTTPException:
2195 return 'error'
2196
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002197 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002198 return 'closed'
2199
2200 cq_label = data['labels'].get('Commit-Queue', {})
2201 if cq_label:
2202 # Vote value is a stringified integer, which we expect from 0 to 2.
2203 vote_value = cq_label.get('value', '0')
2204 vote_text = cq_label.get('values', {}).get(vote_value, '')
2205 if vote_text.lower() == 'commit':
2206 return 'commit'
2207
2208 lgtm_label = data['labels'].get('Code-Review', {})
2209 if lgtm_label:
2210 if 'rejected' in lgtm_label:
2211 return 'not lgtm'
2212 if 'approved' in lgtm_label:
2213 return 'lgtm'
2214
2215 if not data.get('reviewers', {}).get('REVIEWER', []):
2216 return 'unsent'
2217
2218 messages = data.get('messages', [])
2219 if messages:
2220 owner = data['owner'].get('_account_id')
2221 last_message_author = messages[-1].get('author', {}).get('_account_id')
2222 if owner != last_message_author:
2223 # Some reply from non-owner.
2224 return 'reply'
2225
2226 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002227
2228 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002229 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002230 return data['revisions'][data['current_revision']]['_number']
2231
2232 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002233 data = self._GetChangeDetail(['CURRENT_REVISION'])
2234 current_rev = data['current_revision']
2235 url = data['revisions'][current_rev]['fetch']['http']['url']
2236 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002237
2238 def UpdateDescriptionRemote(self, description):
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002239 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
2240 description)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002241
2242 def CloseIssue(self):
2243 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2244
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002245 def GetApprovingReviewers(self):
2246 """Returns a list of reviewers approving the change.
2247
2248 Note: not necessarily committers.
2249 """
2250 raise NotImplementedError()
2251
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002252 def SubmitIssue(self, wait_for_merge=True):
2253 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2254 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002255
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002256 def _GetChangeDetail(self, options=None, issue=None):
2257 options = options or []
2258 issue = issue or self.GetIssue()
2259 assert issue, 'issue required to query Gerrit'
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002260 return gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2261 options)
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002262
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002263 def CMDLand(self, force, bypass_hooks, verbose):
2264 if git_common.is_dirty_git_tree('land'):
2265 return 1
2266 differs = True
2267 last_upload = RunGit(['config',
2268 'branch.%s.gerritsquashhash' % self.GetBranch()],
2269 error_ok=True).strip()
2270 # Note: git diff outputs nothing if there is no diff.
2271 if not last_upload or RunGit(['diff', last_upload]).strip():
2272 print('WARNING: some changes from local branch haven\'t been uploaded')
2273 else:
2274 detail = self._GetChangeDetail(['CURRENT_REVISION'])
2275 if detail['current_revision'] == last_upload:
2276 differs = False
2277 else:
2278 print('WARNING: local branch contents differ from latest uploaded '
2279 'patchset')
2280 if differs:
2281 if not force:
2282 ask_for_data(
2283 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2284 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2285 elif not bypass_hooks:
2286 hook_results = self.RunHook(
2287 committing=True,
2288 may_prompt=not force,
2289 verbose=verbose,
2290 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2291 if not hook_results.should_continue():
2292 return 1
2293
2294 self.SubmitIssue(wait_for_merge=True)
2295 print('Issue %s has been submitted.' % self.GetIssueURL())
2296 return 0
2297
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002298 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2299 directory):
2300 assert not reject
2301 assert not nocommit
2302 assert not directory
2303 assert parsed_issue_arg.valid
2304
2305 self._changelist.issue = parsed_issue_arg.issue
2306
2307 if parsed_issue_arg.hostname:
2308 self._gerrit_host = parsed_issue_arg.hostname
2309 self._gerrit_server = 'https://%s' % self._gerrit_host
2310
2311 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2312
2313 if not parsed_issue_arg.patchset:
2314 # Use current revision by default.
2315 revision_info = detail['revisions'][detail['current_revision']]
2316 patchset = int(revision_info['_number'])
2317 else:
2318 patchset = parsed_issue_arg.patchset
2319 for revision_info in detail['revisions'].itervalues():
2320 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2321 break
2322 else:
2323 DieWithError('Couldn\'t find patchset %i in issue %i' %
2324 (parsed_issue_arg.patchset, self.GetIssue()))
2325
2326 fetch_info = revision_info['fetch']['http']
2327 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2328 RunGit(['cherry-pick', 'FETCH_HEAD'])
2329 self.SetIssue(self.GetIssue())
2330 self.SetPatchset(patchset)
2331 print('Committed patch for issue %i pathset %i locally' %
2332 (self.GetIssue(), self.GetPatchset()))
2333 return 0
2334
2335 @staticmethod
2336 def ParseIssueURL(parsed_url):
2337 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2338 return None
2339 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2340 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2341 # Short urls like https://domain/<issue_number> can be used, but don't allow
2342 # specifying the patchset (you'd 404), but we allow that here.
2343 if parsed_url.path == '/':
2344 part = parsed_url.fragment
2345 else:
2346 part = parsed_url.path
2347 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2348 if match:
2349 return _ParsedIssueNumberArgument(
2350 issue=int(match.group(2)),
2351 patchset=int(match.group(4)) if match.group(4) else None,
2352 hostname=parsed_url.netloc)
2353 return None
2354
tandrii16e0b4e2016-06-07 10:34:28 -07002355 def _GerritCommitMsgHookCheck(self, offer_removal):
2356 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2357 if not os.path.exists(hook):
2358 return
2359 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2360 # custom developer made one.
2361 data = gclient_utils.FileRead(hook)
2362 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2363 return
2364 print('Warning: you have Gerrit commit-msg hook installed.\n'
2365 'It is not neccessary for uploading with git cl in squash mode, '
2366 'and may interfere with it in subtle ways.\n'
2367 'We recommend you remove the commit-msg hook.')
2368 if offer_removal:
2369 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2370 if reply.lower().startswith('y'):
2371 gclient_utils.rm_file_or_tree(hook)
2372 print('Gerrit commit-msg hook removed.')
2373 else:
2374 print('OK, will keep Gerrit commit-msg hook in place.')
2375
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002376 def CMDUploadChange(self, options, args, change):
2377 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002378 if options.squash and options.no_squash:
2379 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002380
2381 if not options.squash and not options.no_squash:
2382 # Load default for user, repo, squash=true, in this order.
2383 options.squash = settings.GetSquashGerritUploads()
2384 elif options.no_squash:
2385 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002386
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002387 # We assume the remote called "origin" is the one we want.
2388 # It is probably not worthwhile to support different workflows.
2389 gerrit_remote = 'origin'
2390
2391 remote, remote_branch = self.GetRemoteBranch()
2392 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2393 pending_prefix='')
2394
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002395 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002396 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002397 if not self.GetIssue():
2398 # TODO(tandrii): deperecate this after 2016Q2. Backwards compatibility
2399 # with shadow branch, which used to contain change-id for a given
2400 # branch, using which we can fetch actual issue number and set it as the
2401 # property of the branch, which is the new way.
2402 message = RunGitSilent([
2403 'show', '--format=%B', '-s',
2404 'refs/heads/git_cl_uploads/%s' % self.GetBranch()])
2405 if message:
2406 change_ids = git_footers.get_footer_change_id(message.strip())
2407 if change_ids and len(change_ids) == 1:
2408 details = self._GetChangeDetail(issue=change_ids[0])
2409 if details:
2410 print('WARNING: found old upload in branch git_cl_uploads/%s '
2411 'corresponding to issue %s' %
2412 (self.GetBranch(), details['_number']))
2413 self.SetIssue(details['_number'])
2414 if not self.GetIssue():
2415 DieWithError(
2416 '\n' # For readability of the blob below.
2417 'Found old upload in branch git_cl_uploads/%s, '
2418 'but failed to find corresponding Gerrit issue.\n'
2419 'If you know the issue number, set it manually first:\n'
2420 ' git cl issue 123456\n'
2421 'If you intended to upload this CL as new issue, '
2422 'just delete or rename the old upload branch:\n'
2423 ' git rename-branch git_cl_uploads/%s old_upload-%s\n'
2424 'After that, please run git cl upload again.' %
2425 tuple([self.GetBranch()] * 3))
2426 # End of backwards compatability.
2427
2428 if self.GetIssue():
2429 # Try to get the message from a previous upload.
2430 message = self.GetDescription()
2431 if not message:
2432 DieWithError(
2433 'failed to fetch description from current Gerrit issue %d\n'
2434 '%s' % (self.GetIssue(), self.GetIssueURL()))
2435 change_id = self._GetChangeDetail()['change_id']
2436 while True:
2437 footer_change_ids = git_footers.get_footer_change_id(message)
2438 if footer_change_ids == [change_id]:
2439 break
2440 if not footer_change_ids:
2441 message = git_footers.add_footer_change_id(message, change_id)
2442 print('WARNING: appended missing Change-Id to issue description')
2443 continue
2444 # There is already a valid footer but with different or several ids.
2445 # Doing this automatically is non-trivial as we don't want to lose
2446 # existing other footers, yet we want to append just 1 desired
2447 # Change-Id. Thus, just create a new footer, but let user verify the
2448 # new description.
2449 message = '%s\n\nChange-Id: %s' % (message, change_id)
2450 print(
2451 'WARNING: issue %s has Change-Id footer(s):\n'
2452 ' %s\n'
2453 'but issue has Change-Id %s, according to Gerrit.\n'
2454 'Please, check the proposed correction to the description, '
2455 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2456 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2457 change_id))
2458 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2459 if not options.force:
2460 change_desc = ChangeDescription(message)
2461 change_desc.prompt()
2462 message = change_desc.description
2463 if not message:
2464 DieWithError("Description is empty. Aborting...")
2465 # Continue the while loop.
2466 # Sanity check of this code - we should end up with proper message
2467 # footer.
2468 assert [change_id] == git_footers.get_footer_change_id(message)
2469 change_desc = ChangeDescription(message)
2470 else:
2471 change_desc = ChangeDescription(
2472 options.message or CreateDescriptionFromLog(args))
2473 if not options.force:
2474 change_desc.prompt()
2475 if not change_desc.description:
2476 DieWithError("Description is empty. Aborting...")
2477 message = change_desc.description
2478 change_ids = git_footers.get_footer_change_id(message)
2479 if len(change_ids) > 1:
2480 DieWithError('too many Change-Id footers, at most 1 allowed.')
2481 if not change_ids:
2482 # Generate the Change-Id automatically.
2483 message = git_footers.add_footer_change_id(
2484 message, GenerateGerritChangeId(message))
2485 change_desc.set_description(message)
2486 change_ids = git_footers.get_footer_change_id(message)
2487 assert len(change_ids) == 1
2488 change_id = change_ids[0]
2489
2490 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2491 if remote is '.':
2492 # If our upstream branch is local, we base our squashed commit on its
2493 # squashed version.
2494 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2495 # Check the squashed hash of the parent.
2496 parent = RunGit(['config',
2497 'branch.%s.gerritsquashhash' % upstream_branch_name],
2498 error_ok=True).strip()
2499 # Verify that the upstream branch has been uploaded too, otherwise
2500 # Gerrit will create additional CLs when uploading.
2501 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2502 RunGitSilent(['rev-parse', parent + ':'])):
2503 # TODO(tandrii): remove "old depot_tools" part on April 12, 2016.
2504 DieWithError(
2505 'Upload upstream branch %s first.\n'
2506 'Note: maybe you\'ve uploaded it with --no-squash or with an old '
2507 'version of depot_tools. If so, then re-upload it with:\n'
2508 ' git cl upload --squash\n' % upstream_branch_name)
2509 else:
2510 parent = self.GetCommonAncestorWithUpstream()
2511
2512 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2513 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2514 '-m', message]).strip()
2515 else:
2516 change_desc = ChangeDescription(
2517 options.message or CreateDescriptionFromLog(args))
2518 if not change_desc.description:
2519 DieWithError("Description is empty. Aborting...")
2520
2521 if not git_footers.get_footer_change_id(change_desc.description):
2522 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002523 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2524 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002525 ref_to_push = 'HEAD'
2526 parent = '%s/%s' % (gerrit_remote, branch)
2527 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2528
2529 assert change_desc
2530 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2531 ref_to_push)]).splitlines()
2532 if len(commits) > 1:
2533 print('WARNING: This will upload %d commits. Run the following command '
2534 'to see which commits will be uploaded: ' % len(commits))
2535 print('git log %s..%s' % (parent, ref_to_push))
2536 print('You can also use `git squash-branch` to squash these into a '
2537 'single commit.')
2538 ask_for_data('About to upload; enter to confirm.')
2539
2540 if options.reviewers or options.tbr_owners:
2541 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2542 change)
2543
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002544 # Extra options that can be specified at push time. Doc:
2545 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2546 refspec_opts = []
2547 if options.title:
2548 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2549 # reverse on its side.
2550 if '_' in options.title:
2551 print('WARNING: underscores in title will be converted to spaces.')
2552 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2553
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002554 if options.send_mail:
2555 if not change_desc.get_reviewers():
2556 DieWithError('Must specify reviewers to send email.')
2557 refspec_opts.append('notify=ALL')
2558 else:
2559 refspec_opts.append('notify=NONE')
2560
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002561 cc = self.GetCCList().split(',')
2562 if options.cc:
2563 cc.extend(options.cc)
2564 cc = filter(None, cc)
2565 if cc:
tandrii@chromium.org074c2af2016-06-03 23:18:40 +00002566 refspec_opts.extend('cc=' + email.strip() for email in cc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002567
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002568 if change_desc.get_reviewers():
2569 refspec_opts.extend('r=' + email.strip()
2570 for email in change_desc.get_reviewers())
2571
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002572 refspec_suffix = ''
2573 if refspec_opts:
2574 refspec_suffix = '%' + ','.join(refspec_opts)
2575 assert ' ' not in refspec_suffix, (
2576 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002577 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002578
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002579 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002580 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002581 print_stdout=True,
2582 # Flush after every line: useful for seeing progress when running as
2583 # recipe.
2584 filter_fn=lambda _: sys.stdout.flush())
2585
2586 if options.squash:
2587 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2588 change_numbers = [m.group(1)
2589 for m in map(regex.match, push_stdout.splitlines())
2590 if m]
2591 if len(change_numbers) != 1:
2592 DieWithError(
2593 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2594 'Change-Id: %s') % (len(change_numbers), change_id))
2595 self.SetIssue(change_numbers[0])
2596 RunGit(['config', 'branch.%s.gerritsquashhash' % self.GetBranch(),
2597 ref_to_push])
2598 return 0
2599
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002600 def _AddChangeIdToCommitMessage(self, options, args):
2601 """Re-commits using the current message, assumes the commit hook is in
2602 place.
2603 """
2604 log_desc = options.message or CreateDescriptionFromLog(args)
2605 git_command = ['commit', '--amend', '-m', log_desc]
2606 RunGit(git_command)
2607 new_log_desc = CreateDescriptionFromLog(args)
2608 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002609 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002610 return new_log_desc
2611 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002612 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002613
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002614 def SetCQState(self, new_state):
2615 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
2616 # TODO(tandrii): maybe allow configurability in codereview.settings or by
2617 # self-discovery of label config for this CL using REST API.
2618 vote_map = {
2619 _CQState.NONE: 0,
2620 _CQState.DRY_RUN: 1,
2621 _CQState.COMMIT : 2,
2622 }
2623 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2624 labels={'Commit-Queue': vote_map[new_state]})
2625
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002626
2627_CODEREVIEW_IMPLEMENTATIONS = {
2628 'rietveld': _RietveldChangelistImpl,
2629 'gerrit': _GerritChangelistImpl,
2630}
2631
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002632
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002633def _add_codereview_select_options(parser):
2634 """Appends --gerrit and --rietveld options to force specific codereview."""
2635 parser.codereview_group = optparse.OptionGroup(
2636 parser, 'EXPERIMENTAL! Codereview override options')
2637 parser.add_option_group(parser.codereview_group)
2638 parser.codereview_group.add_option(
2639 '--gerrit', action='store_true',
2640 help='Force the use of Gerrit for codereview')
2641 parser.codereview_group.add_option(
2642 '--rietveld', action='store_true',
2643 help='Force the use of Rietveld for codereview')
2644
2645
2646def _process_codereview_select_options(parser, options):
2647 if options.gerrit and options.rietveld:
2648 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2649 options.forced_codereview = None
2650 if options.gerrit:
2651 options.forced_codereview = 'gerrit'
2652 elif options.rietveld:
2653 options.forced_codereview = 'rietveld'
2654
2655
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002656class ChangeDescription(object):
2657 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002658 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002659 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002660
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002661 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002662 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002663
agable@chromium.org42c20792013-09-12 17:34:49 +00002664 @property # www.logilab.org/ticket/89786
2665 def description(self): # pylint: disable=E0202
2666 return '\n'.join(self._description_lines)
2667
2668 def set_description(self, desc):
2669 if isinstance(desc, basestring):
2670 lines = desc.splitlines()
2671 else:
2672 lines = [line.rstrip() for line in desc]
2673 while lines and not lines[0]:
2674 lines.pop(0)
2675 while lines and not lines[-1]:
2676 lines.pop(-1)
2677 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002678
piman@chromium.org336f9122014-09-04 02:16:55 +00002679 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002680 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002681 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002682 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002683 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002684 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002685
agable@chromium.org42c20792013-09-12 17:34:49 +00002686 # Get the set of R= and TBR= lines and remove them from the desciption.
2687 regexp = re.compile(self.R_LINE)
2688 matches = [regexp.match(line) for line in self._description_lines]
2689 new_desc = [l for i, l in enumerate(self._description_lines)
2690 if not matches[i]]
2691 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002692
agable@chromium.org42c20792013-09-12 17:34:49 +00002693 # Construct new unified R= and TBR= lines.
2694 r_names = []
2695 tbr_names = []
2696 for match in matches:
2697 if not match:
2698 continue
2699 people = cleanup_list([match.group(2).strip()])
2700 if match.group(1) == 'TBR':
2701 tbr_names.extend(people)
2702 else:
2703 r_names.extend(people)
2704 for name in r_names:
2705 if name not in reviewers:
2706 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002707 if add_owners_tbr:
2708 owners_db = owners.Database(change.RepositoryRoot(),
2709 fopen=file, os_path=os.path, glob=glob.glob)
2710 all_reviewers = set(tbr_names + reviewers)
2711 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2712 all_reviewers)
2713 tbr_names.extend(owners_db.reviewers_for(missing_files,
2714 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002715 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2716 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2717
2718 # Put the new lines in the description where the old first R= line was.
2719 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2720 if 0 <= line_loc < len(self._description_lines):
2721 if new_tbr_line:
2722 self._description_lines.insert(line_loc, new_tbr_line)
2723 if new_r_line:
2724 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002725 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002726 if new_r_line:
2727 self.append_footer(new_r_line)
2728 if new_tbr_line:
2729 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002730
2731 def prompt(self):
2732 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002733 self.set_description([
2734 '# Enter a description of the change.',
2735 '# This will be displayed on the codereview site.',
2736 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002737 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002738 '--------------------',
2739 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002740
agable@chromium.org42c20792013-09-12 17:34:49 +00002741 regexp = re.compile(self.BUG_LINE)
2742 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00002743 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00002744 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002745 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002746 if not content:
2747 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002748 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002749
2750 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002751 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2752 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002753 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002754 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002755
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002756 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00002757 """Adds a footer line to the description.
2758
2759 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
2760 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
2761 that Gerrit footers are always at the end.
2762 """
2763 parsed_footer_line = git_footers.parse_footer(line)
2764 if parsed_footer_line:
2765 # Line is a gerrit footer in the form: Footer-Key: any value.
2766 # Thus, must be appended observing Gerrit footer rules.
2767 self.set_description(
2768 git_footers.add_footer(self.description,
2769 key=parsed_footer_line[0],
2770 value=parsed_footer_line[1]))
2771 return
2772
2773 if not self._description_lines:
2774 self._description_lines.append(line)
2775 return
2776
2777 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
2778 if gerrit_footers:
2779 # git_footers.split_footers ensures that there is an empty line before
2780 # actual (gerrit) footers, if any. We have to keep it that way.
2781 assert top_lines and top_lines[-1] == ''
2782 top_lines, separator = top_lines[:-1], top_lines[-1:]
2783 else:
2784 separator = [] # No need for separator if there are no gerrit_footers.
2785
2786 prev_line = top_lines[-1] if top_lines else ''
2787 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
2788 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2789 top_lines.append('')
2790 top_lines.append(line)
2791 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002792
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002793 def get_reviewers(self):
2794 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002795 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
2796 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002797 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002798
2799
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002800def get_approving_reviewers(props):
2801 """Retrieves the reviewers that approved a CL from the issue properties with
2802 messages.
2803
2804 Note that the list may contain reviewers that are not committer, thus are not
2805 considered by the CQ.
2806 """
2807 return sorted(
2808 set(
2809 message['sender']
2810 for message in props['messages']
2811 if message['approval'] and message['sender'] in props['reviewers']
2812 )
2813 )
2814
2815
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002816def FindCodereviewSettingsFile(filename='codereview.settings'):
2817 """Finds the given file starting in the cwd and going up.
2818
2819 Only looks up to the top of the repository unless an
2820 'inherit-review-settings-ok' file exists in the root of the repository.
2821 """
2822 inherit_ok_file = 'inherit-review-settings-ok'
2823 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002824 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002825 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2826 root = '/'
2827 while True:
2828 if filename in os.listdir(cwd):
2829 if os.path.isfile(os.path.join(cwd, filename)):
2830 return open(os.path.join(cwd, filename))
2831 if cwd == root:
2832 break
2833 cwd = os.path.dirname(cwd)
2834
2835
2836def LoadCodereviewSettingsFromFile(fileobj):
2837 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002838 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002839
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002840 def SetProperty(name, setting, unset_error_ok=False):
2841 fullname = 'rietveld.' + name
2842 if setting in keyvals:
2843 RunGit(['config', fullname, keyvals[setting]])
2844 else:
2845 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2846
2847 SetProperty('server', 'CODE_REVIEW_SERVER')
2848 # Only server setting is required. Other settings can be absent.
2849 # In that case, we ignore errors raised during option deletion attempt.
2850 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002851 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002852 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2853 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002854 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002855 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002856 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2857 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002858 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002859 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002860 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002861 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2862 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002863
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002864 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002865 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002866
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002867 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07002868 RunGit(['config', 'gerrit.squash-uploads',
2869 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002870
tandrii@chromium.org28253532016-04-14 13:46:56 +00002871 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00002872 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00002873 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
2874
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002875 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2876 #should be of the form
2877 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2878 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2879 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2880 keyvals['ORIGIN_URL_CONFIG']])
2881
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002882
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002883def urlretrieve(source, destination):
2884 """urllib is broken for SSL connections via a proxy therefore we
2885 can't use urllib.urlretrieve()."""
2886 with open(destination, 'w') as f:
2887 f.write(urllib2.urlopen(source).read())
2888
2889
ukai@chromium.org712d6102013-11-27 00:52:58 +00002890def hasSheBang(fname):
2891 """Checks fname is a #! script."""
2892 with open(fname) as f:
2893 return f.read(2).startswith('#!')
2894
2895
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00002896# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
2897def DownloadHooks(*args, **kwargs):
2898 pass
2899
2900
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002901def DownloadGerritHook(force):
2902 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002903
2904 Args:
2905 force: True to update hooks. False to install hooks if not present.
2906 """
2907 if not settings.GetIsGerrit():
2908 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00002909 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002910 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2911 if not os.access(dst, os.X_OK):
2912 if os.path.exists(dst):
2913 if not force:
2914 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002915 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002916 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002917 if not hasSheBang(dst):
2918 DieWithError('Not a script: %s\n'
2919 'You need to download from\n%s\n'
2920 'into .git/hooks/commit-msg and '
2921 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002922 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2923 except Exception:
2924 if os.path.exists(dst):
2925 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002926 DieWithError('\nFailed to download hooks.\n'
2927 'You need to download from\n%s\n'
2928 'into .git/hooks/commit-msg and '
2929 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002930
2931
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002932
2933def GetRietveldCodereviewSettingsInteractively():
2934 """Prompt the user for settings."""
2935 server = settings.GetDefaultServerUrl(error_ok=True)
2936 prompt = 'Rietveld server (host[:port])'
2937 prompt += ' [%s]' % (server or DEFAULT_SERVER)
2938 newserver = ask_for_data(prompt + ':')
2939 if not server and not newserver:
2940 newserver = DEFAULT_SERVER
2941 if newserver:
2942 newserver = gclient_utils.UpgradeToHttps(newserver)
2943 if newserver != server:
2944 RunGit(['config', 'rietveld.server', newserver])
2945
2946 def SetProperty(initial, caption, name, is_url):
2947 prompt = caption
2948 if initial:
2949 prompt += ' ("x" to clear) [%s]' % initial
2950 new_val = ask_for_data(prompt + ':')
2951 if new_val == 'x':
2952 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
2953 elif new_val:
2954 if is_url:
2955 new_val = gclient_utils.UpgradeToHttps(new_val)
2956 if new_val != initial:
2957 RunGit(['config', 'rietveld.' + name, new_val])
2958
2959 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
2960 SetProperty(settings.GetDefaultPrivateFlag(),
2961 'Private flag (rietveld only)', 'private', False)
2962 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
2963 'tree-status-url', False)
2964 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
2965 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
2966 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
2967 'run-post-upload-hook', False)
2968
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002969@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002970def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002971 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002972
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002973 print('WARNING: git cl config works for Rietveld only.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002974 'For Gerrit, see http://crbug.com/603116.')
2975 # TODO(tandrii): add Gerrit support as part of http://crbug.com/603116.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00002976 parser.add_option('--activate-update', action='store_true',
2977 help='activate auto-updating [rietveld] section in '
2978 '.git/config')
2979 parser.add_option('--deactivate-update', action='store_true',
2980 help='deactivate auto-updating [rietveld] section in '
2981 '.git/config')
2982 options, args = parser.parse_args(args)
2983
2984 if options.deactivate_update:
2985 RunGit(['config', 'rietveld.autoupdate', 'false'])
2986 return
2987
2988 if options.activate_update:
2989 RunGit(['config', '--unset', 'rietveld.autoupdate'])
2990 return
2991
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002992 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002993 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002994 return 0
2995
2996 url = args[0]
2997 if not url.endswith('codereview.settings'):
2998 url = os.path.join(url, 'codereview.settings')
2999
3000 # Load code review settings and download hooks (if available).
3001 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3002 return 0
3003
3004
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003005def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003006 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003007 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3008 branch = ShortBranchName(branchref)
3009 _, args = parser.parse_args(args)
3010 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003011 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003012 return RunGit(['config', 'branch.%s.base-url' % branch],
3013 error_ok=False).strip()
3014 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003015 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003016 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3017 error_ok=False).strip()
3018
3019
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003020def color_for_status(status):
3021 """Maps a Changelist status to color, for CMDstatus and other tools."""
3022 return {
3023 'unsent': Fore.RED,
3024 'waiting': Fore.BLUE,
3025 'reply': Fore.YELLOW,
3026 'lgtm': Fore.GREEN,
3027 'commit': Fore.MAGENTA,
3028 'closed': Fore.CYAN,
3029 'error': Fore.WHITE,
3030 }.get(status, Fore.WHITE)
3031
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003032
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003033def get_cl_statuses(changes, fine_grained, max_processes=None):
3034 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003035
3036 If fine_grained is true, this will fetch CL statuses from the server.
3037 Otherwise, simply indicate if there's a matching url for the given branches.
3038
3039 If max_processes is specified, it is used as the maximum number of processes
3040 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3041 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003042
3043 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003044 """
3045 # Silence upload.py otherwise it becomes unwieldly.
3046 upload.verbosity = 0
3047
3048 if fine_grained:
3049 # Process one branch synchronously to work through authentication, then
3050 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003051 if changes:
3052 fetch = lambda cl: (cl, cl.GetStatus())
3053 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003054
kmarshall3bff56b2016-06-06 18:31:47 -07003055 if not changes:
3056 # Exit early if there was only one branch to fetch.
3057 return
3058
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003059 changes_to_fetch = changes[1:]
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003060 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003061 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003062 if max_processes is not None
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003063 else len(changes_to_fetch))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003064
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003065 fetched_cls = set()
3066 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003067 while True:
3068 try:
3069 row = it.next(timeout=5)
3070 except multiprocessing.TimeoutError:
3071 break
3072
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003073 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003074 yield row
3075
3076 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003077 for cl in set(changes_to_fetch) - fetched_cls:
3078 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003079
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003080 else:
3081 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003082 for cl in changes:
3083 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003084
rmistry@google.com2dd99862015-06-22 12:22:18 +00003085
3086def upload_branch_deps(cl, args):
3087 """Uploads CLs of local branches that are dependents of the current branch.
3088
3089 If the local branch dependency tree looks like:
3090 test1 -> test2.1 -> test3.1
3091 -> test3.2
3092 -> test2.2 -> test3.3
3093
3094 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3095 run on the dependent branches in this order:
3096 test2.1, test3.1, test3.2, test2.2, test3.3
3097
3098 Note: This function does not rebase your local dependent branches. Use it when
3099 you make a change to the parent branch that will not conflict with its
3100 dependent branches, and you would like their dependencies updated in
3101 Rietveld.
3102 """
3103 if git_common.is_dirty_git_tree('upload-branch-deps'):
3104 return 1
3105
3106 root_branch = cl.GetBranch()
3107 if root_branch is None:
3108 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3109 'Get on a branch!')
3110 if not cl.GetIssue() or not cl.GetPatchset():
3111 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3112 'patchset dependencies without an uploaded CL.')
3113
3114 branches = RunGit(['for-each-ref',
3115 '--format=%(refname:short) %(upstream:short)',
3116 'refs/heads'])
3117 if not branches:
3118 print('No local branches found.')
3119 return 0
3120
3121 # Create a dictionary of all local branches to the branches that are dependent
3122 # on it.
3123 tracked_to_dependents = collections.defaultdict(list)
3124 for b in branches.splitlines():
3125 tokens = b.split()
3126 if len(tokens) == 2:
3127 branch_name, tracked = tokens
3128 tracked_to_dependents[tracked].append(branch_name)
3129
vapiera7fbd5a2016-06-16 09:17:49 -07003130 print()
3131 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003132 dependents = []
3133 def traverse_dependents_preorder(branch, padding=''):
3134 dependents_to_process = tracked_to_dependents.get(branch, [])
3135 padding += ' '
3136 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003137 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003138 dependents.append(dependent)
3139 traverse_dependents_preorder(dependent, padding)
3140 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003141 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003142
3143 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003144 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003145 return 0
3146
vapiera7fbd5a2016-06-16 09:17:49 -07003147 print('This command will checkout all dependent branches and run '
3148 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003149 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3150
andybons@chromium.org962f9462016-02-03 20:00:42 +00003151 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003152 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003153 args.extend(['-t', 'Updated patchset dependency'])
3154
rmistry@google.com2dd99862015-06-22 12:22:18 +00003155 # Record all dependents that failed to upload.
3156 failures = {}
3157 # Go through all dependents, checkout the branch and upload.
3158 try:
3159 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003160 print()
3161 print('--------------------------------------')
3162 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003163 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003164 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003165 try:
3166 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003167 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003168 failures[dependent_branch] = 1
3169 except: # pylint: disable=W0702
3170 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003171 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003172 finally:
3173 # Swap back to the original root branch.
3174 RunGit(['checkout', '-q', root_branch])
3175
vapiera7fbd5a2016-06-16 09:17:49 -07003176 print()
3177 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003178 for dependent_branch in dependents:
3179 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003180 print(' %s : %s' % (dependent_branch, upload_status))
3181 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003182
3183 return 0
3184
3185
kmarshall3bff56b2016-06-06 18:31:47 -07003186def CMDarchive(parser, args):
3187 """Archives and deletes branches associated with closed changelists."""
3188 parser.add_option(
3189 '-j', '--maxjobs', action='store', type=int,
3190 help='The maximum number of jobs to use when retrieving review status')
3191 parser.add_option(
3192 '-f', '--force', action='store_true',
3193 help='Bypasses the confirmation prompt.')
3194
3195 auth.add_auth_options(parser)
3196 options, args = parser.parse_args(args)
3197 if args:
3198 parser.error('Unsupported args: %s' % ' '.join(args))
3199 auth_config = auth.extract_auth_config_from_options(options)
3200
3201 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3202 if not branches:
3203 return 0
3204
vapiera7fbd5a2016-06-16 09:17:49 -07003205 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003206 changes = [Changelist(branchref=b, auth_config=auth_config)
3207 for b in branches.splitlines()]
3208 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3209 statuses = get_cl_statuses(changes,
3210 fine_grained=True,
3211 max_processes=options.maxjobs)
3212 proposal = [(cl.GetBranch(),
3213 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3214 for cl, status in statuses
3215 if status == 'closed']
3216 proposal.sort()
3217
3218 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003219 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003220 return 0
3221
3222 current_branch = GetCurrentBranch()
3223
vapiera7fbd5a2016-06-16 09:17:49 -07003224 print('\nBranches with closed issues that will be archived:\n')
3225 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
kmarshall3bff56b2016-06-06 18:31:47 -07003226 for next_item in proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003227 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003228
3229 if any(branch == current_branch for branch, _ in proposal):
3230 print('You are currently on a branch \'%s\' which is associated with a '
3231 'closed codereview issue, so archive cannot proceed. Please '
3232 'checkout another branch and run this command again.' %
3233 current_branch)
3234 return 1
3235
3236 if not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003237 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3238 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003239 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003240 return 1
3241
3242 for branch, tagname in proposal:
3243 RunGit(['tag', tagname, branch])
3244 RunGit(['branch', '-D', branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003245 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003246
3247 return 0
3248
3249
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003250def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003251 """Show status of changelists.
3252
3253 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003254 - Red not sent for review or broken
3255 - Blue waiting for review
3256 - Yellow waiting for you to reply to review
3257 - Green LGTM'ed
3258 - Magenta in the commit queue
3259 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003260
3261 Also see 'git cl comments'.
3262 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003263 parser.add_option('--field',
3264 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003265 parser.add_option('-f', '--fast', action='store_true',
3266 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003267 parser.add_option(
3268 '-j', '--maxjobs', action='store', type=int,
3269 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003270
3271 auth.add_auth_options(parser)
3272 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003273 if args:
3274 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003275 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003276
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003277 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003278 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003279 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003280 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003281 elif options.field == 'id':
3282 issueid = cl.GetIssue()
3283 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003284 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003285 elif options.field == 'patch':
3286 patchset = cl.GetPatchset()
3287 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003288 print(patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003289 elif options.field == 'url':
3290 url = cl.GetIssueURL()
3291 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003292 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003293 return 0
3294
3295 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3296 if not branches:
3297 print('No local branch found.')
3298 return 0
3299
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003300 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003301 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003302 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003303 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003304 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003305 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003306 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003307
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003308 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003309 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3310 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3311 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003312 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003313 c, status = output.next()
3314 branch_statuses[c.GetBranch()] = status
3315 status = branch_statuses.pop(branch)
3316 url = cl.GetIssueURL()
3317 if url and (not status or status == 'error'):
3318 # The issue probably doesn't exist anymore.
3319 url += ' (broken)'
3320
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003321 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003322 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003323 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003324 color = ''
3325 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003326 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003327 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003328 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003329 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003330
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003331 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003332 print()
3333 print('Current branch:',)
3334 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003335 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003336 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003337 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003338 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003339 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003340 print('Issue description:')
3341 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003342 return 0
3343
3344
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003345def colorize_CMDstatus_doc():
3346 """To be called once in main() to add colors to git cl status help."""
3347 colors = [i for i in dir(Fore) if i[0].isupper()]
3348
3349 def colorize_line(line):
3350 for color in colors:
3351 if color in line.upper():
3352 # Extract whitespaces first and the leading '-'.
3353 indent = len(line) - len(line.lstrip(' ')) + 1
3354 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3355 return line
3356
3357 lines = CMDstatus.__doc__.splitlines()
3358 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3359
3360
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003361@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003362def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003363 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003364
3365 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003366 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003367 parser.add_option('-r', '--reverse', action='store_true',
3368 help='Lookup the branch(es) for the specified issues. If '
3369 'no issues are specified, all branches with mapped '
3370 'issues will be listed.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003371 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003372 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003373 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003374
dnj@chromium.org406c4402015-03-03 17:22:28 +00003375 if options.reverse:
3376 branches = RunGit(['for-each-ref', 'refs/heads',
3377 '--format=%(refname:short)']).splitlines()
3378
3379 # Reverse issue lookup.
3380 issue_branch_map = {}
3381 for branch in branches:
3382 cl = Changelist(branchref=branch)
3383 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3384 if not args:
3385 args = sorted(issue_branch_map.iterkeys())
3386 for issue in args:
3387 if not issue:
3388 continue
vapiera7fbd5a2016-06-16 09:17:49 -07003389 print('Branch for issue number %s: %s' % (
3390 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
dnj@chromium.org406c4402015-03-03 17:22:28 +00003391 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003392 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003393 if len(args) > 0:
3394 try:
3395 issue = int(args[0])
3396 except ValueError:
3397 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003398 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003399 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003400 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003401 return 0
3402
3403
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003404def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003405 """Shows or posts review comments for any changelist."""
3406 parser.add_option('-a', '--add-comment', dest='comment',
3407 help='comment to add to an issue')
3408 parser.add_option('-i', dest='issue',
3409 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003410 parser.add_option('-j', '--json-file',
3411 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003412 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003413 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003414 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003415
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003416 issue = None
3417 if options.issue:
3418 try:
3419 issue = int(options.issue)
3420 except ValueError:
3421 DieWithError('A review issue id is expected to be a number')
3422
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003423 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003424
3425 if options.comment:
3426 cl.AddComment(options.comment)
3427 return 0
3428
3429 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003430 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003431 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003432 summary.append({
3433 'date': message['date'],
3434 'lgtm': False,
3435 'message': message['text'],
3436 'not_lgtm': False,
3437 'sender': message['sender'],
3438 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003439 if message['disapproval']:
3440 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003441 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003442 elif message['approval']:
3443 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003444 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003445 elif message['sender'] == data['owner_email']:
3446 color = Fore.MAGENTA
3447 else:
3448 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003449 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003450 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003451 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003452 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003453 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003454 if options.json_file:
3455 with open(options.json_file, 'wb') as f:
3456 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003457 return 0
3458
3459
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003460@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003461def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003462 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003463 parser.add_option('-d', '--display', action='store_true',
3464 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003465 parser.add_option('-n', '--new-description',
3466 help='New description to set for this issue (- for stdin)')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003467
3468 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003469 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003470 options, args = parser.parse_args(args)
3471 _process_codereview_select_options(parser, options)
3472
3473 target_issue = None
3474 if len(args) > 0:
3475 issue_arg = ParseIssueNumberArgument(args[0])
3476 if not issue_arg.valid:
3477 parser.print_help()
3478 return 1
3479 target_issue = issue_arg.issue
3480
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003481 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003482
3483 cl = Changelist(
3484 auth_config=auth_config, issue=target_issue,
3485 codereview=options.forced_codereview)
3486
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003487 if not cl.GetIssue():
3488 DieWithError('This branch has no associated changelist.')
3489 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003490
smut@google.com34fb6b12015-07-13 20:03:26 +00003491 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003492 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003493 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003494
3495 if options.new_description:
3496 text = options.new_description
3497 if text == '-':
3498 text = '\n'.join(l.rstrip() for l in sys.stdin)
3499
3500 description.set_description(text)
3501 else:
3502 description.prompt()
3503
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003504 if cl.GetDescription() != description.description:
3505 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003506 return 0
3507
3508
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003509def CreateDescriptionFromLog(args):
3510 """Pulls out the commit log to use as a base for the CL description."""
3511 log_args = []
3512 if len(args) == 1 and not args[0].endswith('.'):
3513 log_args = [args[0] + '..']
3514 elif len(args) == 1 and args[0].endswith('...'):
3515 log_args = [args[0][:-1]]
3516 elif len(args) == 2:
3517 log_args = [args[0] + '..' + args[1]]
3518 else:
3519 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003520 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003521
3522
thestig@chromium.org44202a22014-03-11 19:22:18 +00003523def CMDlint(parser, args):
3524 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003525 parser.add_option('--filter', action='append', metavar='-x,+y',
3526 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003527 auth.add_auth_options(parser)
3528 options, args = parser.parse_args(args)
3529 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003530
3531 # Access to a protected member _XX of a client class
3532 # pylint: disable=W0212
3533 try:
3534 import cpplint
3535 import cpplint_chromium
3536 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003537 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003538 return 1
3539
3540 # Change the current working directory before calling lint so that it
3541 # shows the correct base.
3542 previous_cwd = os.getcwd()
3543 os.chdir(settings.GetRoot())
3544 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003545 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003546 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3547 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003548 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003549 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003550 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003551
3552 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003553 command = args + files
3554 if options.filter:
3555 command = ['--filter=' + ','.join(options.filter)] + command
3556 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003557
3558 white_regex = re.compile(settings.GetLintRegex())
3559 black_regex = re.compile(settings.GetLintIgnoreRegex())
3560 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3561 for filename in filenames:
3562 if white_regex.match(filename):
3563 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07003564 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003565 else:
3566 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3567 extra_check_functions)
3568 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003569 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003570 finally:
3571 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07003572 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003573 if cpplint._cpplint_state.error_count != 0:
3574 return 1
3575 return 0
3576
3577
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003578def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003579 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003580 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003581 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003582 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003583 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003584 auth.add_auth_options(parser)
3585 options, args = parser.parse_args(args)
3586 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003587
sbc@chromium.org71437c02015-04-09 19:29:40 +00003588 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07003589 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003590 return 1
3591
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003592 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003593 if args:
3594 base_branch = args[0]
3595 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003596 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003597 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003598
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003599 cl.RunHook(
3600 committing=not options.upload,
3601 may_prompt=False,
3602 verbose=options.verbose,
3603 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003604 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003605
3606
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003607def GenerateGerritChangeId(message):
3608 """Returns Ixxxxxx...xxx change id.
3609
3610 Works the same way as
3611 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3612 but can be called on demand on all platforms.
3613
3614 The basic idea is to generate git hash of a state of the tree, original commit
3615 message, author/committer info and timestamps.
3616 """
3617 lines = []
3618 tree_hash = RunGitSilent(['write-tree'])
3619 lines.append('tree %s' % tree_hash.strip())
3620 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3621 if code == 0:
3622 lines.append('parent %s' % parent.strip())
3623 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3624 lines.append('author %s' % author.strip())
3625 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3626 lines.append('committer %s' % committer.strip())
3627 lines.append('')
3628 # Note: Gerrit's commit-hook actually cleans message of some lines and
3629 # whitespace. This code is not doing this, but it clearly won't decrease
3630 # entropy.
3631 lines.append(message)
3632 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3633 stdin='\n'.join(lines))
3634 return 'I%s' % change_hash.strip()
3635
3636
wittman@chromium.org455dc922015-01-26 20:15:50 +00003637def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3638 """Computes the remote branch ref to use for the CL.
3639
3640 Args:
3641 remote (str): The git remote for the CL.
3642 remote_branch (str): The git remote branch for the CL.
3643 target_branch (str): The target branch specified by the user.
3644 pending_prefix (str): The pending prefix from the settings.
3645 """
3646 if not (remote and remote_branch):
3647 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003648
wittman@chromium.org455dc922015-01-26 20:15:50 +00003649 if target_branch:
3650 # Cannonicalize branch references to the equivalent local full symbolic
3651 # refs, which are then translated into the remote full symbolic refs
3652 # below.
3653 if '/' not in target_branch:
3654 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3655 else:
3656 prefix_replacements = (
3657 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3658 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3659 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3660 )
3661 match = None
3662 for regex, replacement in prefix_replacements:
3663 match = re.search(regex, target_branch)
3664 if match:
3665 remote_branch = target_branch.replace(match.group(0), replacement)
3666 break
3667 if not match:
3668 # This is a branch path but not one we recognize; use as-is.
3669 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003670 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3671 # Handle the refs that need to land in different refs.
3672 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003673
wittman@chromium.org455dc922015-01-26 20:15:50 +00003674 # Create the true path to the remote branch.
3675 # Does the following translation:
3676 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3677 # * refs/remotes/origin/master -> refs/heads/master
3678 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3679 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3680 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3681 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3682 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3683 'refs/heads/')
3684 elif remote_branch.startswith('refs/remotes/branch-heads'):
3685 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3686 # If a pending prefix exists then replace refs/ with it.
3687 if pending_prefix:
3688 remote_branch = remote_branch.replace('refs/', pending_prefix)
3689 return remote_branch
3690
3691
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003692def cleanup_list(l):
3693 """Fixes a list so that comma separated items are put as individual items.
3694
3695 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3696 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3697 """
3698 items = sum((i.split(',') for i in l), [])
3699 stripped_items = (i.strip() for i in items)
3700 return sorted(filter(None, stripped_items))
3701
3702
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003703@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003704def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003705 """Uploads the current changelist to codereview.
3706
3707 Can skip dependency patchset uploads for a branch by running:
3708 git config branch.branch_name.skip-deps-uploads True
3709 To unset run:
3710 git config --unset branch.branch_name.skip-deps-uploads
3711 Can also set the above globally by using the --global flag.
3712 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003713 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3714 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003715 parser.add_option('--bypass-watchlists', action='store_true',
3716 dest='bypass_watchlists',
3717 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003718 parser.add_option('-f', action='store_true', dest='force',
3719 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003720 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003721 parser.add_option('-t', dest='title',
3722 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003723 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003724 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003725 help='reviewer email addresses')
3726 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003727 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003728 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003729 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003730 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003731 parser.add_option('--emulate_svn_auto_props',
3732 '--emulate-svn-auto-props',
3733 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003734 dest="emulate_svn_auto_props",
3735 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003736 parser.add_option('-c', '--use-commit-queue', action='store_true',
3737 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003738 parser.add_option('--private', action='store_true',
3739 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003740 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003741 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003742 metavar='TARGET',
3743 help='Apply CL to remote ref TARGET. ' +
3744 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003745 parser.add_option('--squash', action='store_true',
3746 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003747 parser.add_option('--no-squash', action='store_true',
3748 help='Don\'t squash multiple commits into one ' +
3749 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003750 parser.add_option('--email', default=None,
3751 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003752 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3753 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003754 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3755 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003756 help='Send the patchset to do a CQ dry run right after '
3757 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003758 parser.add_option('--dependencies', action='store_true',
3759 help='Uploads CLs of all the local branches that depend on '
3760 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003761
rmistry@google.com2dd99862015-06-22 12:22:18 +00003762 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003763 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003764 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003765 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003766 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003767 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003768 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003769
sbc@chromium.org71437c02015-04-09 19:29:40 +00003770 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003771 return 1
3772
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003773 options.reviewers = cleanup_list(options.reviewers)
3774 options.cc = cleanup_list(options.cc)
3775
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003776 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3777 settings.GetIsGerrit()
3778
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003779 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00003780 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003781
3782
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003783def IsSubmoduleMergeCommit(ref):
3784 # When submodules are added to the repo, we expect there to be a single
3785 # non-git-svn merge commit at remote HEAD with a signature comment.
3786 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003787 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003788 return RunGit(cmd) != ''
3789
3790
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003791def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003792 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003793
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003794 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3795 upstream and closes the issue automatically and atomically.
3796
3797 Otherwise (in case of Rietveld):
3798 Squashes branch into a single commit.
3799 Updates changelog with metadata (e.g. pointer to review).
3800 Pushes/dcommits the code upstream.
3801 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003802 """
3803 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3804 help='bypass upload presubmit hook')
3805 parser.add_option('-m', dest='message',
3806 help="override review description")
3807 parser.add_option('-f', action='store_true', dest='force',
3808 help="force yes to questions (don't prompt)")
3809 parser.add_option('-c', dest='contributor',
3810 help="external contributor for patch (appended to " +
3811 "description and used as author for git). Should be " +
3812 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003813 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003814 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003815 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003816 auth_config = auth.extract_auth_config_from_options(options)
3817
3818 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003819
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003820 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3821 if cl.IsGerrit():
3822 if options.message:
3823 # This could be implemented, but it requires sending a new patch to
3824 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3825 # Besides, Gerrit has the ability to change the commit message on submit
3826 # automatically, thus there is no need to support this option (so far?).
3827 parser.error('-m MESSAGE option is not supported for Gerrit.')
3828 if options.contributor:
3829 parser.error(
3830 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3831 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3832 'the contributor\'s "name <email>". If you can\'t upload such a '
3833 'commit for review, contact your repository admin and request'
3834 '"Forge-Author" permission.')
3835 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3836 options.verbose)
3837
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003838 current = cl.GetBranch()
3839 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3840 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07003841 print()
3842 print('Attempting to push branch %r into another local branch!' % current)
3843 print()
3844 print('Either reparent this branch on top of origin/master:')
3845 print(' git reparent-branch --root')
3846 print()
3847 print('OR run `git rebase-update` if you think the parent branch is ')
3848 print('already committed.')
3849 print()
3850 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003851 return 1
3852
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003853 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003854 # Default to merging against our best guess of the upstream branch.
3855 args = [cl.GetUpstreamBranch()]
3856
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003857 if options.contributor:
3858 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07003859 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003860 return 1
3861
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003862 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003863 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003864
sbc@chromium.org71437c02015-04-09 19:29:40 +00003865 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003866 return 1
3867
3868 # This rev-list syntax means "show all commits not in my branch that
3869 # are in base_branch".
3870 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3871 base_branch]).splitlines()
3872 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07003873 print('Base branch "%s" has %d commits '
3874 'not in this branch.' % (base_branch, len(upstream_commits)))
3875 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003876 return 1
3877
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003878 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003879 svn_head = None
3880 if cmd == 'dcommit' or base_has_submodules:
3881 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
3882 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003883
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003884 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003885 # If the base_head is a submodule merge commit, the first parent of the
3886 # base_head should be a git-svn commit, which is what we're interested in.
3887 base_svn_head = base_branch
3888 if base_has_submodules:
3889 base_svn_head += '^1'
3890
3891 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003892 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07003893 print('This branch has %d additional commits not upstreamed yet.'
3894 % len(extra_commits.splitlines()))
3895 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
3896 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003897 return 1
3898
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003899 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003900 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003901 author = None
3902 if options.contributor:
3903 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003904 hook_results = cl.RunHook(
3905 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003906 may_prompt=not options.force,
3907 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003908 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003909 if not hook_results.should_continue():
3910 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003911
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003912 # Check the tree status if the tree status URL is set.
3913 status = GetTreeStatus()
3914 if 'closed' == status:
3915 print('The tree is closed. Please wait for it to reopen. Use '
3916 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3917 return 1
3918 elif 'unknown' == status:
3919 print('Unable to determine tree status. Please verify manually and '
3920 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3921 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003922
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003923 change_desc = ChangeDescription(options.message)
3924 if not change_desc.description and cl.GetIssue():
3925 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003926
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003927 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00003928 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003929 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00003930 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003931 print('No description set.')
3932 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00003933 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003934
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003935 # Keep a separate copy for the commit message, because the commit message
3936 # contains the link to the Rietveld issue, while the Rietveld message contains
3937 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003938 # Keep a separate copy for the commit message.
3939 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00003940 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003941
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003942 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00003943 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00003944 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00003945 # after it. Add a period on a new line to circumvent this. Also add a space
3946 # before the period to make sure that Gitiles continues to correctly resolve
3947 # the URL.
3948 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003949 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003950 commit_desc.append_footer('Patch from %s.' % options.contributor)
3951
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00003952 print('Description:')
3953 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003954
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003955 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003956 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00003957 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003958
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003959 # We want to squash all this branch's commits into one commit with the proper
3960 # description. We do this by doing a "reset --soft" to the base branch (which
3961 # keeps the working copy the same), then dcommitting that. If origin/master
3962 # has a submodule merge commit, we'll also need to cherry-pick the squashed
3963 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003964 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003965 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
3966 # Delete the branches if they exist.
3967 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
3968 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
3969 result = RunGitWithCode(showref_cmd)
3970 if result[0] == 0:
3971 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003972
3973 # We might be in a directory that's present in this branch but not in the
3974 # trunk. Move up to the top of the tree so that git commands that expect a
3975 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003976 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003977 if rel_base_path:
3978 os.chdir(rel_base_path)
3979
3980 # Stuff our change into the merge branch.
3981 # We wrap in a try...finally block so if anything goes wrong,
3982 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003983 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003984 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003985 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003986 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003987 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00003988 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003989 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003990 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003991 RunGit(
3992 [
3993 'commit', '--author', options.contributor,
3994 '-m', commit_desc.description,
3995 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003996 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003997 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003998 if base_has_submodules:
3999 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
4000 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
4001 RunGit(['checkout', CHERRY_PICK_BRANCH])
4002 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004003 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004004 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004005 mirror = settings.GetGitMirror(remote)
4006 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004007 pending_prefix = settings.GetPendingRefPrefix()
4008 if not pending_prefix or branch.startswith(pending_prefix):
4009 # If not using refs/pending/heads/* at all, or target ref is already set
4010 # to pending, then push to the target ref directly.
4011 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004012 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004013 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004014 else:
4015 # Cherry-pick the change on top of pending ref and then push it.
4016 assert branch.startswith('refs/'), branch
4017 assert pending_prefix[-1] == '/', pending_prefix
4018 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004019 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004020 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004021 if retcode == 0:
4022 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004023 else:
4024 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004025 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00004026 'svn', 'dcommit',
4027 '-C%s' % options.similarity,
4028 '--no-rebase', '--rmdir',
4029 ]
4030 if settings.GetForceHttpsCommitUrl():
4031 # Allow forcing https commit URLs for some projects that don't allow
4032 # committing to http URLs (like Google Code).
4033 remote_url = cl.GetGitSvnRemoteUrl()
4034 if urlparse.urlparse(remote_url).scheme == 'http':
4035 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004036 cmd_args.append('--commit-url=%s' % remote_url)
4037 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004038 if 'Committed r' in output:
4039 revision = re.match(
4040 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4041 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004042 finally:
4043 # And then swap back to the original branch and clean up.
4044 RunGit(['checkout', '-q', cl.GetBranch()])
4045 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004046 if base_has_submodules:
4047 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004048
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004049 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004050 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004051 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004052
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004053 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004054 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004055 try:
4056 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4057 # We set pushed_to_pending to False, since it made it all the way to the
4058 # real ref.
4059 pushed_to_pending = False
4060 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004061 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004062
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004063 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004064 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004065 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004066 if not to_pending:
4067 if viewvc_url and revision:
4068 change_desc.append_footer(
4069 'Committed: %s%s' % (viewvc_url, revision))
4070 elif revision:
4071 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004072 print('Closing issue '
4073 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004074 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004075 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004076 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004077 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004078 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004079 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004080 if options.bypass_hooks:
4081 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4082 else:
4083 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004084 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004085 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004086
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004087 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004088 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004089 print('The commit is in the pending queue (%s).' % pending_ref)
4090 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4091 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004092
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004093 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4094 if os.path.isfile(hook):
4095 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004096
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004097 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004098
4099
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004100def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004101 print()
4102 print('Waiting for commit to be landed on %s...' % real_ref)
4103 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004104 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4105 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004106 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004107
4108 loop = 0
4109 while True:
4110 sys.stdout.write('fetching (%d)... \r' % loop)
4111 sys.stdout.flush()
4112 loop += 1
4113
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004114 if mirror:
4115 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004116 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4117 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4118 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4119 for commit in commits.splitlines():
4120 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004121 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004122 return commit
4123
4124 current_rev = to_rev
4125
4126
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004127def PushToGitPending(remote, pending_ref, upstream_ref):
4128 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4129
4130 Returns:
4131 (retcode of last operation, output log of last operation).
4132 """
4133 assert pending_ref.startswith('refs/'), pending_ref
4134 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4135 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4136 code = 0
4137 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004138 max_attempts = 3
4139 attempts_left = max_attempts
4140 while attempts_left:
4141 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004142 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004143 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004144
4145 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004146 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004147 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004148 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004149 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004150 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004151 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004152 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004153 continue
4154
4155 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004156 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004157 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004158 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004159 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004160 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4161 'the following files have merge conflicts:' % pending_ref)
4162 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4163 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004164 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004165 return code, out
4166
4167 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004168 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004169 code, out = RunGitWithCode(
4170 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4171 if code == 0:
4172 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004173 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004174 return code, out
4175
vapiera7fbd5a2016-06-16 09:17:49 -07004176 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004177 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004178 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004179 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004180 print('Fatal push error. Make sure your .netrc credentials and git '
4181 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004182 return code, out
4183
vapiera7fbd5a2016-06-16 09:17:49 -07004184 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004185 return code, out
4186
4187
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004188def IsFatalPushFailure(push_stdout):
4189 """True if retrying push won't help."""
4190 return '(prohibited by Gerrit)' in push_stdout
4191
4192
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004193@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004194def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004195 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004196 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004197 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004198 # If it looks like previous commits were mirrored with git-svn.
4199 message = """This repository appears to be a git-svn mirror, but no
4200upstream SVN master is set. You probably need to run 'git auto-svn' once."""
4201 else:
4202 message = """This doesn't appear to be an SVN repository.
4203If your project has a true, writeable git repository, you probably want to run
4204'git cl land' instead.
4205If your project has a git mirror of an upstream SVN master, you probably need
4206to run 'git svn init'.
4207
4208Using the wrong command might cause your commit to appear to succeed, and the
4209review to be closed, without actually landing upstream. If you choose to
4210proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004211 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004212 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
tandrii3bb82ff2016-06-17 07:36:36 -07004213 # TODO(tandrii): kill this post SVN migration with
4214 # https://codereview.chromium.org/2076683002
4215 print('WARNING: chrome infrastructure is migrating SVN repos to Git.\n'
4216 'Please let us know of this project you are committing to:'
4217 ' http://crbug.com/600451')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004218 return SendUpstream(parser, args, 'dcommit')
4219
4220
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004221@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004222def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004223 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004224 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004225 print('This appears to be an SVN repository.')
4226 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004227 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004228 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004229 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004230
4231
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004232@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004233def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004234 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004235 parser.add_option('-b', dest='newbranch',
4236 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004237 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004238 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004239 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4240 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004241 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004242 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004243 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004244 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004245 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004246 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004247
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004248
4249 group = optparse.OptionGroup(
4250 parser,
4251 'Options for continuing work on the current issue uploaded from a '
4252 'different clone (e.g. different machine). Must be used independently '
4253 'from the other options. No issue number should be specified, and the '
4254 'branch must have an issue number associated with it')
4255 group.add_option('--reapply', action='store_true', dest='reapply',
4256 help='Reset the branch and reapply the issue.\n'
4257 'CAUTION: This will undo any local changes in this '
4258 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004259
4260 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004261 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004262 parser.add_option_group(group)
4263
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004264 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004265 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004266 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004267 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004268 auth_config = auth.extract_auth_config_from_options(options)
4269
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004270
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004271 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004272 if options.newbranch:
4273 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004274 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004275 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004276
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004277 cl = Changelist(auth_config=auth_config,
4278 codereview=options.forced_codereview)
4279 if not cl.GetIssue():
4280 parser.error('current branch must have an associated issue')
4281
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004282 upstream = cl.GetUpstreamBranch()
4283 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004284 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004285
4286 RunGit(['reset', '--hard', upstream])
4287 if options.pull:
4288 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004289
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004290 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4291 options.directory)
4292
4293 if len(args) != 1 or not args[0]:
4294 parser.error('Must specify issue number or url')
4295
4296 # We don't want uncommitted changes mixed up with the patch.
4297 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004298 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004299
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004300 if options.newbranch:
4301 if options.force:
4302 RunGit(['branch', '-D', options.newbranch],
4303 stderr=subprocess2.PIPE, error_ok=True)
4304 RunGit(['new-branch', options.newbranch])
4305
4306 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4307
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004308 if cl.IsGerrit():
4309 if options.reject:
4310 parser.error('--reject is not supported with Gerrit codereview.')
4311 if options.nocommit:
4312 parser.error('--nocommit is not supported with Gerrit codereview.')
4313 if options.directory:
4314 parser.error('--directory is not supported with Gerrit codereview.')
4315
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004316 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004317 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004318
4319
4320def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004321 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004322 # Provide a wrapper for git svn rebase to help avoid accidental
4323 # git svn dcommit.
4324 # It's the only command that doesn't use parser at all since we just defer
4325 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004326
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004327 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004328
4329
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004330def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004331 """Fetches the tree status and returns either 'open', 'closed',
4332 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004333 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004334 if url:
4335 status = urllib2.urlopen(url).read().lower()
4336 if status.find('closed') != -1 or status == '0':
4337 return 'closed'
4338 elif status.find('open') != -1 or status == '1':
4339 return 'open'
4340 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004341 return 'unset'
4342
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004343
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004344def GetTreeStatusReason():
4345 """Fetches the tree status from a json url and returns the message
4346 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004347 url = settings.GetTreeStatusUrl()
4348 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004349 connection = urllib2.urlopen(json_url)
4350 status = json.loads(connection.read())
4351 connection.close()
4352 return status['message']
4353
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004354
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004355def GetBuilderMaster(bot_list):
4356 """For a given builder, fetch the master from AE if available."""
4357 map_url = 'https://builders-map.appspot.com/'
4358 try:
4359 master_map = json.load(urllib2.urlopen(map_url))
4360 except urllib2.URLError as e:
4361 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4362 (map_url, e))
4363 except ValueError as e:
4364 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4365 if not master_map:
4366 return None, 'Failed to build master map.'
4367
4368 result_master = ''
4369 for bot in bot_list:
4370 builder = bot.split(':', 1)[0]
4371 master_list = master_map.get(builder, [])
4372 if not master_list:
4373 return None, ('No matching master for builder %s.' % builder)
4374 elif len(master_list) > 1:
4375 return None, ('The builder name %s exists in multiple masters %s.' %
4376 (builder, master_list))
4377 else:
4378 cur_master = master_list[0]
4379 if not result_master:
4380 result_master = cur_master
4381 elif result_master != cur_master:
4382 return None, 'The builders do not belong to the same master.'
4383 return result_master, None
4384
4385
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004386def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004387 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004388 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004389 status = GetTreeStatus()
4390 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004391 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004392 return 2
4393
vapiera7fbd5a2016-06-16 09:17:49 -07004394 print('The tree is %s' % status)
4395 print()
4396 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004397 if status != 'open':
4398 return 1
4399 return 0
4400
4401
maruel@chromium.org15192402012-09-06 12:38:29 +00004402def CMDtry(parser, args):
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004403 """Triggers try jobs through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00004404 group = optparse.OptionGroup(parser, "Try job options")
4405 group.add_option(
4406 "-b", "--bot", action="append",
4407 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
4408 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004409 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00004410 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004411 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00004412 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004413 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00004414 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004415 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004416 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004417 "-r", "--revision",
4418 help="Revision to use for the try job; default: the "
4419 "revision will be determined by the try server; see "
4420 "its waterfall for more info")
4421 group.add_option(
4422 "-c", "--clobber", action="store_true", default=False,
4423 help="Force a clobber before building; e.g. don't do an "
4424 "incremental build")
4425 group.add_option(
4426 "--project",
4427 help="Override which project to use. Projects are defined "
4428 "server-side to define what default bot set to use")
4429 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004430 "-p", "--property", dest="properties", action="append", default=[],
4431 help="Specify generic properties in the form -p key1=value1 -p "
4432 "key2=value2 etc (buildbucket only). The value will be treated as "
4433 "json if decodable, or as string otherwise.")
4434 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004435 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004436 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004437 "--use-rietveld", action="store_true", default=False,
4438 help="Use Rietveld to trigger try jobs.")
4439 group.add_option(
4440 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4441 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004442 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004443 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004444 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004445 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004446
machenbach@chromium.org45453142015-09-15 08:45:22 +00004447 if options.use_rietveld and options.properties:
4448 parser.error('Properties can only be specified with buildbucket')
4449
4450 # Make sure that all properties are prop=value pairs.
4451 bad_params = [x for x in options.properties if '=' not in x]
4452 if bad_params:
4453 parser.error('Got properties with missing "=": %s' % bad_params)
4454
maruel@chromium.org15192402012-09-06 12:38:29 +00004455 if args:
4456 parser.error('Unknown arguments: %s' % args)
4457
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004458 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004459 if not cl.GetIssue():
4460 parser.error('Need to upload first')
4461
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004462 if cl.IsGerrit():
4463 parser.error(
4464 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4465 'If your project has Commit Queue, dry run is a workaround:\n'
4466 ' git cl set-commit --dry-run')
4467 # Code below assumes Rietveld issue.
4468 # TODO(tandrii): actually implement for Gerrit http://crbug.com/599931.
4469
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004470 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004471 if props.get('closed'):
4472 parser.error('Cannot send tryjobs for a closed CL')
4473
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004474 if props.get('private'):
4475 parser.error('Cannot use trybots with private issue')
4476
maruel@chromium.org15192402012-09-06 12:38:29 +00004477 if not options.name:
4478 options.name = cl.GetBranch()
4479
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004480 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004481 options.master, err_msg = GetBuilderMaster(options.bot)
4482 if err_msg:
4483 parser.error('Tryserver master cannot be found because: %s\n'
4484 'Please manually specify the tryserver master'
4485 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004486
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004487 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004488 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004489 if not options.bot:
4490 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004491
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004492 # Get try masters from PRESUBMIT.py files.
4493 masters = presubmit_support.DoGetTryMasters(
4494 change,
4495 change.LocalPaths(),
4496 settings.GetRoot(),
4497 None,
4498 None,
4499 options.verbose,
4500 sys.stdout)
4501 if masters:
4502 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004503
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004504 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4505 options.bot = presubmit_support.DoGetTrySlaves(
4506 change,
4507 change.LocalPaths(),
4508 settings.GetRoot(),
4509 None,
4510 None,
4511 options.verbose,
4512 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004513
4514 if not options.bot:
4515 # Get try masters from cq.cfg if any.
4516 # TODO(tandrii): some (but very few) projects store cq.cfg in different
4517 # location.
4518 cq_cfg = os.path.join(change.RepositoryRoot(),
4519 'infra', 'config', 'cq.cfg')
4520 if os.path.exists(cq_cfg):
4521 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00004522 cq_masters = commit_queue.get_master_builder_map(
4523 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004524 for master, builders in cq_masters.iteritems():
4525 for builder in builders:
4526 # Skip presubmit builders, because these will fail without LGTM.
machenbach@chromium.org2403e802016-04-29 12:34:42 +00004527 masters.setdefault(master, {})[builder] = ['defaulttests']
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004528 if masters:
tandriib93dd2b2016-06-07 08:03:08 -07004529 print('Loaded default bots from CQ config (%s)' % cq_cfg)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004530 return masters
tandriib93dd2b2016-06-07 08:03:08 -07004531 else:
4532 print('CQ config exists (%s) but has no try bots listed' % cq_cfg)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004533
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004534 if not options.bot:
4535 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00004536
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004537 builders_and_tests = {}
4538 # TODO(machenbach): The old style command-line options don't support
4539 # multiple try masters yet.
4540 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4541 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4542
4543 for bot in old_style:
4544 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004545 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004546 elif ',' in bot:
4547 parser.error('Specify one bot per --bot flag')
4548 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004549 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004550
4551 for bot, tests in new_style:
4552 builders_and_tests.setdefault(bot, []).extend(tests)
4553
4554 # Return a master map with one master to be backwards compatible. The
4555 # master name defaults to an empty string, which will cause the master
4556 # not to be set on rietveld (deprecated).
4557 return {options.master: builders_and_tests}
4558
4559 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004560
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004561 for builders in masters.itervalues():
4562 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004563 print('ERROR You are trying to send a job to a triggered bot. This type '
4564 'of bot requires an\ninitial job from a parent (usually a builder).'
4565 ' Instead send your job to the parent.\n'
4566 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004567 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004568
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004569 patchset = cl.GetMostRecentPatchset()
4570 if patchset and patchset != cl.GetPatchset():
4571 print(
4572 '\nWARNING Mismatch between local config and server. Did a previous '
4573 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4574 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004575 if options.luci:
4576 trigger_luci_job(cl, masters, options)
4577 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004578 try:
4579 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4580 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004581 print('ERROR: %s' % ex)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004582 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004583 except Exception as e:
4584 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
vapiera7fbd5a2016-06-16 09:17:49 -07004585 print('ERROR: Exception when trying to trigger tryjobs: %s\n%s' %
4586 (e, stacktrace))
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004587 return 1
4588 else:
4589 try:
4590 cl.RpcServer().trigger_distributed_try_jobs(
4591 cl.GetIssue(), patchset, options.name, options.clobber,
4592 options.revision, masters)
4593 except urllib2.HTTPError as e:
4594 if e.code == 404:
4595 print('404 from rietveld; '
4596 'did you mean to use "git try" instead of "git cl try"?')
4597 return 1
4598 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004599
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004600 for (master, builders) in sorted(masters.iteritems()):
4601 if master:
vapiera7fbd5a2016-06-16 09:17:49 -07004602 print('Master: %s' % master)
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004603 length = max(len(builder) for builder in builders)
4604 for builder in sorted(builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004605 print(' %*s: %s' % (length, builder, ','.join(builders[builder])))
maruel@chromium.org15192402012-09-06 12:38:29 +00004606 return 0
4607
4608
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004609def CMDtry_results(parser, args):
4610 group = optparse.OptionGroup(parser, "Try job results options")
4611 group.add_option(
4612 "-p", "--patchset", type=int, help="patchset number if not current.")
4613 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004614 "--print-master", action='store_true', help="print master name as well.")
4615 group.add_option(
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004616 "--color", action='store_true', default=setup_color.IS_TTY,
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004617 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004618 group.add_option(
4619 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4620 help="Host of buildbucket. The default host is %default.")
4621 parser.add_option_group(group)
4622 auth.add_auth_options(parser)
4623 options, args = parser.parse_args(args)
4624 if args:
4625 parser.error('Unrecognized args: %s' % ' '.join(args))
4626
4627 auth_config = auth.extract_auth_config_from_options(options)
4628 cl = Changelist(auth_config=auth_config)
4629 if not cl.GetIssue():
4630 parser.error('Need to upload first')
4631
4632 if not options.patchset:
4633 options.patchset = cl.GetMostRecentPatchset()
4634 if options.patchset and options.patchset != cl.GetPatchset():
4635 print(
4636 '\nWARNING Mismatch between local config and server. Did a previous '
4637 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4638 'Continuing using\npatchset %s.\n' % options.patchset)
4639 try:
4640 jobs = fetch_try_jobs(auth_config, cl, options)
4641 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004642 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004643 return 1
4644 except Exception as e:
4645 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
vapiera7fbd5a2016-06-16 09:17:49 -07004646 print('ERROR: Exception when trying to fetch tryjobs: %s\n%s' %
4647 (e, stacktrace))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004648 return 1
4649 print_tryjobs(options, jobs)
4650 return 0
4651
4652
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004653@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004654def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004655 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004656 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004657 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004658 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004659
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004660 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004661 if args:
4662 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004663 branch = cl.GetBranch()
4664 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004665 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004666 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004667
4668 # Clear configured merge-base, if there is one.
4669 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004670 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004671 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004672 return 0
4673
4674
thestig@chromium.org00858c82013-12-02 23:08:03 +00004675def CMDweb(parser, args):
4676 """Opens the current CL in the web browser."""
4677 _, args = parser.parse_args(args)
4678 if args:
4679 parser.error('Unrecognized args: %s' % ' '.join(args))
4680
4681 issue_url = Changelist().GetIssueURL()
4682 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004683 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004684 return 1
4685
4686 webbrowser.open(issue_url)
4687 return 0
4688
4689
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004690def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004691 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004692 parser.add_option('-d', '--dry-run', action='store_true',
4693 help='trigger in dry run mode')
4694 parser.add_option('-c', '--clear', action='store_true',
4695 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004696 auth.add_auth_options(parser)
4697 options, args = parser.parse_args(args)
4698 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004699 if args:
4700 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004701 if options.dry_run and options.clear:
4702 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4703
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004704 cl = Changelist(auth_config=auth_config)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004705 if options.clear:
4706 state = _CQState.CLEAR
4707 elif options.dry_run:
4708 state = _CQState.DRY_RUN
4709 else:
4710 state = _CQState.COMMIT
4711 if not cl.GetIssue():
4712 parser.error('Must upload the issue first')
4713 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004714 return 0
4715
4716
groby@chromium.org411034a2013-02-26 15:12:01 +00004717def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004718 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004719 auth.add_auth_options(parser)
4720 options, args = parser.parse_args(args)
4721 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004722 if args:
4723 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004724 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00004725 # Ensure there actually is an issue to close.
4726 cl.GetDescription()
4727 cl.CloseIssue()
4728 return 0
4729
4730
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004731def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004732 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004733 auth.add_auth_options(parser)
4734 options, args = parser.parse_args(args)
4735 auth_config = auth.extract_auth_config_from_options(options)
4736 if args:
4737 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004738
4739 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004740 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004741 # Staged changes would be committed along with the patch from last
4742 # upload, hence counted toward the "last upload" side in the final
4743 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004744 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004745 return 1
4746
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004747 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004748 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004749 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004750 if not issue:
4751 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004752 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004753 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004754
4755 # Create a new branch based on the merge-base
4756 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004757 # Clear cached branch in cl object, to avoid overwriting original CL branch
4758 # properties.
4759 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004760 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004761 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004762 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004763 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004764 return rtn
4765
wychen@chromium.org06928532015-02-03 02:11:29 +00004766 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004767 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00004768 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004769 finally:
4770 RunGit(['checkout', '-q', branch])
4771 RunGit(['branch', '-D', TMP_BRANCH])
4772
4773 return 0
4774
4775
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004776def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004777 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004778 parser.add_option(
4779 '--no-color',
4780 action='store_true',
4781 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004782 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004783 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004784 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004785
4786 author = RunGit(['config', 'user.email']).strip() or None
4787
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004788 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004789
4790 if args:
4791 if len(args) > 1:
4792 parser.error('Unknown args')
4793 base_branch = args[0]
4794 else:
4795 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004796 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004797
4798 change = cl.GetChange(base_branch, None)
4799 return owners_finder.OwnersFinder(
4800 [f.LocalPath() for f in
4801 cl.GetChange(base_branch, None).AffectedFiles()],
4802 change.RepositoryRoot(), author,
4803 fopen=file, os_path=os.path, glob=glob.glob,
4804 disable_color=options.no_color).run()
4805
4806
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004807def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004808 """Generates a diff command."""
4809 # Generate diff for the current branch's changes.
4810 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4811 upstream_commit, '--' ]
4812
4813 if args:
4814 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004815 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004816 diff_cmd.append(arg)
4817 else:
4818 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004819
4820 return diff_cmd
4821
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004822def MatchingFileType(file_name, extensions):
4823 """Returns true if the file name ends with one of the given extensions."""
4824 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004825
enne@chromium.org555cfe42014-01-29 18:21:39 +00004826@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004827def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004828 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00004829 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07004830 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004831 parser.add_option('--full', action='store_true',
4832 help='Reformat the full content of all touched files')
4833 parser.add_option('--dry-run', action='store_true',
4834 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004835 parser.add_option('--python', action='store_true',
4836 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004837 parser.add_option('--diff', action='store_true',
4838 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004839 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004840
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004841 # git diff generates paths against the root of the repository. Change
4842 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004843 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004844 if rel_base_path:
4845 os.chdir(rel_base_path)
4846
digit@chromium.org29e47272013-05-17 17:01:46 +00004847 # Grab the merge-base commit, i.e. the upstream commit of the current
4848 # branch when it was created or the last time it was rebased. This is
4849 # to cover the case where the user may have called "git fetch origin",
4850 # moving the origin branch to a newer commit, but hasn't rebased yet.
4851 upstream_commit = None
4852 cl = Changelist()
4853 upstream_branch = cl.GetUpstreamBranch()
4854 if upstream_branch:
4855 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4856 upstream_commit = upstream_commit.strip()
4857
4858 if not upstream_commit:
4859 DieWithError('Could not find base commit for this branch. '
4860 'Are you in detached state?')
4861
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004862 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4863 diff_output = RunGit(changed_files_cmd)
4864 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004865 # Filter out files deleted by this CL
4866 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004867
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004868 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4869 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4870 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004871 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004872
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004873 top_dir = os.path.normpath(
4874 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
4875
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004876 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4877 # formatted. This is used to block during the presubmit.
4878 return_value = 0
4879
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004880 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004881 # Locate the clang-format binary in the checkout
4882 try:
4883 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07004884 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004885 DieWithError(e)
4886
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004887 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004888 cmd = [clang_format_tool]
4889 if not opts.dry_run and not opts.diff:
4890 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004891 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004892 if opts.diff:
4893 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004894 else:
4895 env = os.environ.copy()
4896 env['PATH'] = str(os.path.dirname(clang_format_tool))
4897 try:
4898 script = clang_format.FindClangFormatScriptInChromiumTree(
4899 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07004900 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004901 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004902
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004903 cmd = [sys.executable, script, '-p0']
4904 if not opts.dry_run and not opts.diff:
4905 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004906
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004907 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
4908 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004909
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004910 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
4911 if opts.diff:
4912 sys.stdout.write(stdout)
4913 if opts.dry_run and len(stdout) > 0:
4914 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004915
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004916 # Similar code to above, but using yapf on .py files rather than clang-format
4917 # on C/C++ files
4918 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004919 yapf_tool = gclient_utils.FindExecutable('yapf')
4920 if yapf_tool is None:
4921 DieWithError('yapf not found in PATH')
4922
4923 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004924 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004925 cmd = [yapf_tool]
4926 if not opts.dry_run and not opts.diff:
4927 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004928 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004929 if opts.diff:
4930 sys.stdout.write(stdout)
4931 else:
4932 # TODO(sbc): yapf --lines mode still has some issues.
4933 # https://github.com/google/yapf/issues/154
4934 DieWithError('--python currently only works with --full')
4935
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004936 # Dart's formatter does not have the nice property of only operating on
4937 # modified chunks, so hard code full.
4938 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004939 try:
4940 command = [dart_format.FindDartFmtToolInChromiumTree()]
4941 if not opts.dry_run and not opts.diff:
4942 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004943 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004944
ppi@chromium.org6593d932016-03-03 15:41:15 +00004945 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004946 if opts.dry_run and stdout:
4947 return_value = 2
4948 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07004949 print('Warning: Unable to check Dart code formatting. Dart SDK not '
4950 'found in this checkout. Files in other languages are still '
4951 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004952
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004953 # Format GN build files. Always run on full build files for canonical form.
4954 if gn_diff_files:
4955 cmd = ['gn', 'format']
4956 if not opts.dry_run and not opts.diff:
4957 cmd.append('--in-place')
4958 for gn_diff_file in gn_diff_files:
bsep@chromium.org627d9002016-04-29 00:00:52 +00004959 stdout = RunCommand(cmd + [gn_diff_file],
4960 shell=sys.platform == 'win32',
4961 cwd=top_dir)
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004962 if opts.diff:
4963 sys.stdout.write(stdout)
4964
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004965 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004966
4967
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004968@subcommand.usage('<codereview url or issue id>')
4969def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004970 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004971 _, args = parser.parse_args(args)
4972
4973 if len(args) != 1:
4974 parser.print_help()
4975 return 1
4976
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004977 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00004978 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004979 parser.print_help()
4980 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00004981 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004982
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004983 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00004984 output = RunGit(['config', '--local', '--get-regexp',
4985 r'branch\..*\.%s' % issueprefix],
4986 error_ok=True)
4987 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004988 if issue == target_issue:
4989 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004990
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004991 branches = []
4992 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00004993 branches.extend(find_issues(cls.IssueSettingSuffix()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004994 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07004995 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004996 return 1
4997 if len(branches) == 1:
4998 RunGit(['checkout', branches[0]])
4999 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005000 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005001 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005002 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005003 which = raw_input('Choose by index: ')
5004 try:
5005 RunGit(['checkout', branches[int(which)]])
5006 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005007 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005008 return 1
5009
5010 return 0
5011
5012
maruel@chromium.org29404b52014-09-08 22:58:00 +00005013def CMDlol(parser, args):
5014 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005015 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005016 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5017 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5018 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005019 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005020 return 0
5021
5022
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005023class OptionParser(optparse.OptionParser):
5024 """Creates the option parse and add --verbose support."""
5025 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005026 optparse.OptionParser.__init__(
5027 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005028 self.add_option(
5029 '-v', '--verbose', action='count', default=0,
5030 help='Use 2 times for more debugging info')
5031
5032 def parse_args(self, args=None, values=None):
5033 options, args = optparse.OptionParser.parse_args(self, args, values)
5034 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5035 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5036 return options, args
5037
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005038
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005039def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005040 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005041 print('\nYour python version %s is unsupported, please upgrade.\n' %
5042 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005043 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005044
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005045 # Reload settings.
5046 global settings
5047 settings = Settings()
5048
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005049 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005050 dispatcher = subcommand.CommandDispatcher(__name__)
5051 try:
5052 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005053 except auth.AuthenticationError as e:
5054 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005055 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005056 if e.code != 500:
5057 raise
5058 DieWithError(
5059 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5060 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005061 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005062
5063
5064if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005065 # These affect sys.stdout so do it outside of main() to simplify mocks in
5066 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005067 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005068 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005069 try:
5070 sys.exit(main(sys.argv[1:]))
5071 except KeyboardInterrupt:
5072 sys.stderr.write('interrupted\n')
5073 sys.exit(1)