blob: 0edde48d906f0955243a46589706c46ad28131b5 [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
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000010from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000011from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000012import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000013import collections
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000014import glob
sheyang@google.com6ebaf782015-05-12 19:17:54 +000015import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000016import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import logging
18import optparse
19import os
20import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000021import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000023import textwrap
sheyang@google.com6ebaf782015-05-12 19:17:54 +000024import time
25import traceback
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000026import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000027import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000028import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000029import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000030import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000031import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000032
33try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000034 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000035except ImportError:
36 pass
37
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000038from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000039from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000040from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000041import auth
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +000042from luci_hacks import trigger_luci_job as luci_trigger
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000043import clang_format
tandrii@chromium.org71184c02016-01-13 15:18:44 +000044import commit_queue
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000045import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000046import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000047import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000048import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000049import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000050import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000051import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000052import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000053import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000054import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000055import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000056import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000057import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000058import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000059import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000060import watchlists
61
maruel@chromium.org0633fb42013-08-16 20:06:14 +000062__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000063
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000064DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000065POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000066DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000067GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
rmistry@google.comc68112d2015-03-03 12:48:06 +000068REFS_THAT_ALIAS_TO_OTHER_REFS = {
69 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
70 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
71}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000072
thestig@chromium.org44202a22014-03-11 19:22:18 +000073# Valid extensions for files we want to lint.
74DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
75DEFAULT_LINT_IGNORE_REGEX = r"$^"
76
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000077# Shortcut since it quickly becomes redundant.
78Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000079
maruel@chromium.orgddd59412011-11-30 14:20:38 +000080# Initialized in main()
81settings = None
82
83
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000084def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000085 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000086 sys.exit(1)
87
88
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000089def GetNoGitPagerEnv():
90 env = os.environ.copy()
91 # 'cat' is a magical git string that disables pagers on all platforms.
92 env['GIT_PAGER'] = 'cat'
93 return env
94
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000095
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000096def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000097 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000098 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000099 except subprocess2.CalledProcessError as e:
100 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000101 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000102 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000103 'Command "%s" failed.\n%s' % (
104 ' '.join(args), error_message or e.stdout or ''))
105 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000106
107
108def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000109 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000110 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000111
112
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000113def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000114 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000115 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000116 if suppress_stderr:
117 stderr = subprocess2.VOID
118 else:
119 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000120 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000121 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000122 stdout=subprocess2.PIPE,
123 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000124 return code, out[0]
125 except ValueError:
126 # When the subprocess fails, it returns None. That triggers a ValueError
127 # when trying to unpack the return value into (out, code).
128 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000129
130
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000131def RunGitSilent(args):
132 """Returns stdout, suppresses stderr and ingores the return code."""
133 return RunGitWithCode(args, suppress_stderr=True)[1]
134
135
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000136def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000137 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000138 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000139 return (version.startswith(prefix) and
140 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000141
142
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000143def BranchExists(branch):
144 """Return True if specified branch exists."""
145 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
146 suppress_stderr=True)
147 return not code
148
149
maruel@chromium.org90541732011-04-01 17:54:18 +0000150def ask_for_data(prompt):
151 try:
152 return raw_input(prompt)
153 except KeyboardInterrupt:
154 # Hide the exception.
155 sys.exit(1)
156
157
iannucci@chromium.org79540052012-10-19 23:15:26 +0000158def git_set_branch_value(key, value):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000159 branch = GetCurrentBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000160 if not branch:
161 return
162
163 cmd = ['config']
164 if isinstance(value, int):
165 cmd.append('--int')
166 git_key = 'branch.%s.%s' % (branch, key)
167 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000168
169
170def git_get_branch_default(key, default):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000171 branch = GetCurrentBranch()
iannucci@chromium.org79540052012-10-19 23:15:26 +0000172 if branch:
173 git_key = 'branch.%s.%s' % (branch, key)
174 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
175 try:
176 return int(stdout.strip())
177 except ValueError:
178 pass
179 return default
180
181
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000182def add_git_similarity(parser):
183 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000184 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000185 help='Sets the percentage that a pair of files need to match in order to'
186 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000187 parser.add_option(
188 '--find-copies', action='store_true',
189 help='Allows git to look for copies.')
190 parser.add_option(
191 '--no-find-copies', action='store_false', dest='find_copies',
192 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000193
194 old_parser_args = parser.parse_args
195 def Parse(args):
196 options, args = old_parser_args(args)
197
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000198 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000199 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000200 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000201 print('Note: Saving similarity of %d%% in git config.'
202 % options.similarity)
203 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000204
iannucci@chromium.org79540052012-10-19 23:15:26 +0000205 options.similarity = max(0, min(options.similarity, 100))
206
207 if options.find_copies is None:
208 options.find_copies = bool(
209 git_get_branch_default('git-find-copies', True))
210 else:
211 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000212
213 print('Using %d%% similarity for rename/copy detection. '
214 'Override with --similarity.' % options.similarity)
215
216 return options, args
217 parser.parse_args = Parse
218
219
machenbach@chromium.org45453142015-09-15 08:45:22 +0000220def _get_properties_from_options(options):
221 properties = dict(x.split('=', 1) for x in options.properties)
222 for key, val in properties.iteritems():
223 try:
224 properties[key] = json.loads(val)
225 except ValueError:
226 pass # If a value couldn't be evaluated, treat it as a string.
227 return properties
228
229
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000230def _prefix_master(master):
231 """Convert user-specified master name to full master name.
232
233 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
234 name, while the developers always use shortened master name
235 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
236 function does the conversion for buildbucket migration.
237 """
238 prefix = 'master.'
239 if master.startswith(prefix):
240 return master
241 return '%s%s' % (prefix, master)
242
243
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000244def _buildbucket_retry(operation_name, http, *args, **kwargs):
245 """Retries requests to buildbucket service and returns parsed json content."""
246 try_count = 0
247 while True:
248 response, content = http.request(*args, **kwargs)
249 try:
250 content_json = json.loads(content)
251 except ValueError:
252 content_json = None
253
254 # Buildbucket could return an error even if status==200.
255 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000256 error = content_json.get('error')
257 if error.get('code') == 403:
258 raise BuildbucketResponseException(
259 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000260 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000261 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000262 raise BuildbucketResponseException(msg)
263
264 if response.status == 200:
265 if not content_json:
266 raise BuildbucketResponseException(
267 'Buildbucket returns invalid json content: %s.\n'
268 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
269 content)
270 return content_json
271 if response.status < 500 or try_count >= 2:
272 raise httplib2.HttpLib2Error(content)
273
274 # status >= 500 means transient failures.
275 logging.debug('Transient errors when %s. Will retry.', operation_name)
276 time.sleep(0.5 + 1.5*try_count)
277 try_count += 1
278 assert False, 'unreachable'
279
280
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000281def trigger_luci_job(changelist, masters, options):
282 """Send a job to run on LUCI."""
283 issue_props = changelist.GetIssueProperties()
284 issue = changelist.GetIssue()
285 patchset = changelist.GetMostRecentPatchset()
286 for builders_and_tests in sorted(masters.itervalues()):
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000287 # TODO(hinoka et al): add support for other properties.
288 # Currently, this completely ignores testfilter and other properties.
289 for builder in sorted(builders_and_tests):
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000290 luci_trigger.trigger(
291 builder, 'HEAD', issue, patchset, issue_props['project'])
292
293
machenbach@chromium.org45453142015-09-15 08:45:22 +0000294def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000295 rietveld_url = settings.GetDefaultServerUrl()
296 rietveld_host = urlparse.urlparse(rietveld_url).hostname
297 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
298 http = authenticator.authorize(httplib2.Http())
299 http.force_exception_to_status_code = True
300 issue_props = changelist.GetIssueProperties()
301 issue = changelist.GetIssue()
302 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000303 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000304
305 buildbucket_put_url = (
306 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000307 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000308 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
309 hostname=rietveld_host,
310 issue=issue,
311 patch=patchset)
312
313 batch_req_body = {'builds': []}
314 print_text = []
315 print_text.append('Tried jobs on:')
316 for master, builders_and_tests in sorted(masters.iteritems()):
317 print_text.append('Master: %s' % master)
318 bucket = _prefix_master(master)
319 for builder, tests in sorted(builders_and_tests.iteritems()):
320 print_text.append(' %s: %s' % (builder, tests))
321 parameters = {
322 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000323 'changes': [{
324 'author': {'email': issue_props['owner_email']},
325 'revision': options.revision,
326 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000327 'properties': {
328 'category': category,
329 'issue': issue,
330 'master': master,
331 'patch_project': issue_props['project'],
332 'patch_storage': 'rietveld',
333 'patchset': patchset,
334 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000335 'rietveld': rietveld_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000336 },
337 }
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000338 if tests:
339 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000340 if properties:
341 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000342 if options.clobber:
343 parameters['properties']['clobber'] = True
344 batch_req_body['builds'].append(
345 {
346 'bucket': bucket,
347 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000348 'client_operation_id': str(uuid.uuid4()),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000349 'tags': ['builder:%s' % builder,
350 'buildset:%s' % buildset,
351 'master:%s' % master,
352 'user_agent:git_cl_try']
353 }
354 )
355
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000356 _buildbucket_retry(
357 'triggering tryjobs',
358 http,
359 buildbucket_put_url,
360 'PUT',
361 body=json.dumps(batch_req_body),
362 headers={'Content-Type': 'application/json'}
363 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000364 print_text.append('To see results here, run: git cl try-results')
365 print_text.append('To see results in browser, run: git cl web')
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000366 print '\n'.join(print_text)
kjellander@chromium.org44424542015-06-02 18:35:29 +0000367
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000368
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000369def fetch_try_jobs(auth_config, changelist, options):
370 """Fetches tryjobs from buildbucket.
371
372 Returns a map from build id to build info as json dictionary.
373 """
374 rietveld_url = settings.GetDefaultServerUrl()
375 rietveld_host = urlparse.urlparse(rietveld_url).hostname
376 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
377 if authenticator.has_cached_credentials():
378 http = authenticator.authorize(httplib2.Http())
379 else:
380 print ('Warning: Some results might be missing because %s' %
381 # Get the message on how to login.
382 auth.LoginRequiredError(rietveld_host).message)
383 http = httplib2.Http()
384
385 http.force_exception_to_status_code = True
386
387 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
388 hostname=rietveld_host,
389 issue=changelist.GetIssue(),
390 patch=options.patchset)
391 params = {'tag': 'buildset:%s' % buildset}
392
393 builds = {}
394 while True:
395 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
396 hostname=options.buildbucket_host,
397 params=urllib.urlencode(params))
398 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
399 for build in content.get('builds', []):
400 builds[build['id']] = build
401 if 'next_cursor' in content:
402 params['start_cursor'] = content['next_cursor']
403 else:
404 break
405 return builds
406
407
408def print_tryjobs(options, builds):
409 """Prints nicely result of fetch_try_jobs."""
410 if not builds:
411 print 'No tryjobs scheduled'
412 return
413
414 # Make a copy, because we'll be modifying builds dictionary.
415 builds = builds.copy()
416 builder_names_cache = {}
417
418 def get_builder(b):
419 try:
420 return builder_names_cache[b['id']]
421 except KeyError:
422 try:
423 parameters = json.loads(b['parameters_json'])
424 name = parameters['builder_name']
425 except (ValueError, KeyError) as error:
426 print 'WARNING: failed to get builder name for build %s: %s' % (
427 b['id'], error)
428 name = None
429 builder_names_cache[b['id']] = name
430 return name
431
432 def get_bucket(b):
433 bucket = b['bucket']
434 if bucket.startswith('master.'):
435 return bucket[len('master.'):]
436 return bucket
437
438 if options.print_master:
439 name_fmt = '%%-%ds %%-%ds' % (
440 max(len(str(get_bucket(b))) for b in builds.itervalues()),
441 max(len(str(get_builder(b))) for b in builds.itervalues()))
442 def get_name(b):
443 return name_fmt % (get_bucket(b), get_builder(b))
444 else:
445 name_fmt = '%%-%ds' % (
446 max(len(str(get_builder(b))) for b in builds.itervalues()))
447 def get_name(b):
448 return name_fmt % get_builder(b)
449
450 def sort_key(b):
451 return b['status'], b.get('result'), get_name(b), b.get('url')
452
453 def pop(title, f, color=None, **kwargs):
454 """Pop matching builds from `builds` dict and print them."""
455
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000456 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000457 colorize = str
458 else:
459 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
460
461 result = []
462 for b in builds.values():
463 if all(b.get(k) == v for k, v in kwargs.iteritems()):
464 builds.pop(b['id'])
465 result.append(b)
466 if result:
467 print colorize(title)
468 for b in sorted(result, key=sort_key):
469 print ' ', colorize('\t'.join(map(str, f(b))))
470
471 total = len(builds)
472 pop(status='COMPLETED', result='SUCCESS',
473 title='Successes:', color=Fore.GREEN,
474 f=lambda b: (get_name(b), b.get('url')))
475 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
476 title='Infra Failures:', color=Fore.MAGENTA,
477 f=lambda b: (get_name(b), b.get('url')))
478 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
479 title='Failures:', color=Fore.RED,
480 f=lambda b: (get_name(b), b.get('url')))
481 pop(status='COMPLETED', result='CANCELED',
482 title='Canceled:', color=Fore.MAGENTA,
483 f=lambda b: (get_name(b),))
484 pop(status='COMPLETED', result='FAILURE',
485 failure_reason='INVALID_BUILD_DEFINITION',
486 title='Wrong master/builder name:', color=Fore.MAGENTA,
487 f=lambda b: (get_name(b),))
488 pop(status='COMPLETED', result='FAILURE',
489 title='Other failures:',
490 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
491 pop(status='COMPLETED',
492 title='Other finished:',
493 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
494 pop(status='STARTED',
495 title='Started:', color=Fore.YELLOW,
496 f=lambda b: (get_name(b), b.get('url')))
497 pop(status='SCHEDULED',
498 title='Scheduled:',
499 f=lambda b: (get_name(b), 'id=%s' % b['id']))
500 # The last section is just in case buildbucket API changes OR there is a bug.
501 pop(title='Other:',
502 f=lambda b: (get_name(b), 'id=%s' % b['id']))
503 assert len(builds) == 0
504 print 'Total: %d tryjobs' % total
505
506
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000507def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
508 """Return the corresponding git ref if |base_url| together with |glob_spec|
509 matches the full |url|.
510
511 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
512 """
513 fetch_suburl, as_ref = glob_spec.split(':')
514 if allow_wildcards:
515 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
516 if glob_match:
517 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
518 # "branches/{472,597,648}/src:refs/remotes/svn/*".
519 branch_re = re.escape(base_url)
520 if glob_match.group(1):
521 branch_re += '/' + re.escape(glob_match.group(1))
522 wildcard = glob_match.group(2)
523 if wildcard == '*':
524 branch_re += '([^/]*)'
525 else:
526 # Escape and replace surrounding braces with parentheses and commas
527 # with pipe symbols.
528 wildcard = re.escape(wildcard)
529 wildcard = re.sub('^\\\\{', '(', wildcard)
530 wildcard = re.sub('\\\\,', '|', wildcard)
531 wildcard = re.sub('\\\\}$', ')', wildcard)
532 branch_re += wildcard
533 if glob_match.group(3):
534 branch_re += re.escape(glob_match.group(3))
535 match = re.match(branch_re, url)
536 if match:
537 return re.sub('\*$', match.group(1), as_ref)
538
539 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
540 if fetch_suburl:
541 full_url = base_url + '/' + fetch_suburl
542 else:
543 full_url = base_url
544 if full_url == url:
545 return as_ref
546 return None
547
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000548
iannucci@chromium.org79540052012-10-19 23:15:26 +0000549def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000550 """Prints statistics about the change to the user."""
551 # --no-ext-diff is broken in some versions of Git, so try to work around
552 # this by overriding the environment (but there is still a problem if the
553 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000554 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000555 if 'GIT_EXTERNAL_DIFF' in env:
556 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000557
558 if find_copies:
559 similarity_options = ['--find-copies-harder', '-l100000',
560 '-C%s' % similarity]
561 else:
562 similarity_options = ['-M%s' % similarity]
563
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000564 try:
565 stdout = sys.stdout.fileno()
566 except AttributeError:
567 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000568 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000569 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000570 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000571 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000572
573
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000574class BuildbucketResponseException(Exception):
575 pass
576
577
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000578class Settings(object):
579 def __init__(self):
580 self.default_server = None
581 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000582 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000583 self.is_git_svn = None
584 self.svn_branch = None
585 self.tree_status_url = None
586 self.viewvc_url = None
587 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000588 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000589 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000590 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000591 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000592 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000593 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000594 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000595
596 def LazyUpdateIfNeeded(self):
597 """Updates the settings from a codereview.settings file, if available."""
598 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000599 # The only value that actually changes the behavior is
600 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000601 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000602 error_ok=True
603 ).strip().lower()
604
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000605 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000606 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000607 LoadCodereviewSettingsFromFile(cr_settings_file)
608 self.updated = True
609
610 def GetDefaultServerUrl(self, error_ok=False):
611 if not self.default_server:
612 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000613 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000614 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000615 if error_ok:
616 return self.default_server
617 if not self.default_server:
618 error_message = ('Could not find settings file. You must configure '
619 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000620 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000621 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000622 return self.default_server
623
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000624 @staticmethod
625 def GetRelativeRoot():
626 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000627
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000628 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000629 if self.root is None:
630 self.root = os.path.abspath(self.GetRelativeRoot())
631 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000632
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000633 def GetGitMirror(self, remote='origin'):
634 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000635 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000636 if not os.path.isdir(local_url):
637 return None
638 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
639 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
640 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
641 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
642 if mirror.exists():
643 return mirror
644 return None
645
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000646 def GetIsGitSvn(self):
647 """Return true if this repo looks like it's using git-svn."""
648 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000649 if self.GetPendingRefPrefix():
650 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
651 self.is_git_svn = False
652 else:
653 # If you have any "svn-remote.*" config keys, we think you're using svn.
654 self.is_git_svn = RunGitWithCode(
655 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000656 return self.is_git_svn
657
658 def GetSVNBranch(self):
659 if self.svn_branch is None:
660 if not self.GetIsGitSvn():
661 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
662
663 # Try to figure out which remote branch we're based on.
664 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000665 # 1) iterate through our branch history and find the svn URL.
666 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000667
668 # regexp matching the git-svn line that contains the URL.
669 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
670
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000671 # We don't want to go through all of history, so read a line from the
672 # pipe at a time.
673 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000674 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000675 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
676 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000677 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000678 for line in proc.stdout:
679 match = git_svn_re.match(line)
680 if match:
681 url = match.group(1)
682 proc.stdout.close() # Cut pipe.
683 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000684
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000685 if url:
686 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
687 remotes = RunGit(['config', '--get-regexp',
688 r'^svn-remote\..*\.url']).splitlines()
689 for remote in remotes:
690 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000691 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000692 remote = match.group(1)
693 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000694 rewrite_root = RunGit(
695 ['config', 'svn-remote.%s.rewriteRoot' % remote],
696 error_ok=True).strip()
697 if rewrite_root:
698 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000699 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000700 ['config', 'svn-remote.%s.fetch' % remote],
701 error_ok=True).strip()
702 if fetch_spec:
703 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
704 if self.svn_branch:
705 break
706 branch_spec = RunGit(
707 ['config', 'svn-remote.%s.branches' % remote],
708 error_ok=True).strip()
709 if branch_spec:
710 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
711 if self.svn_branch:
712 break
713 tag_spec = RunGit(
714 ['config', 'svn-remote.%s.tags' % remote],
715 error_ok=True).strip()
716 if tag_spec:
717 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
718 if self.svn_branch:
719 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000720
721 if not self.svn_branch:
722 DieWithError('Can\'t guess svn branch -- try specifying it on the '
723 'command line')
724
725 return self.svn_branch
726
727 def GetTreeStatusUrl(self, error_ok=False):
728 if not self.tree_status_url:
729 error_message = ('You must configure your tree status URL by running '
730 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000731 self.tree_status_url = self._GetRietveldConfig(
732 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000733 return self.tree_status_url
734
735 def GetViewVCUrl(self):
736 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000737 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000738 return self.viewvc_url
739
rmistry@google.com90752582014-01-14 21:04:50 +0000740 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000741 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000742
rmistry@google.com78948ed2015-07-08 23:09:57 +0000743 def GetIsSkipDependencyUpload(self, branch_name):
744 """Returns true if specified branch should skip dep uploads."""
745 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
746 error_ok=True)
747
rmistry@google.com5626a922015-02-26 14:03:30 +0000748 def GetRunPostUploadHook(self):
749 run_post_upload_hook = self._GetRietveldConfig(
750 'run-post-upload-hook', error_ok=True)
751 return run_post_upload_hook == "True"
752
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000753 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000754 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000755
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000756 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000757 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000758
ukai@chromium.orge8077812012-02-03 03:41:46 +0000759 def GetIsGerrit(self):
760 """Return true if this repo is assosiated with gerrit code review system."""
761 if self.is_gerrit is None:
762 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
763 return self.is_gerrit
764
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000765 def GetSquashGerritUploads(self):
766 """Return true if uploads to Gerrit should be squashed by default."""
767 if self.squash_gerrit_uploads is None:
768 self.squash_gerrit_uploads = (
769 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
770 error_ok=True).strip() == 'true')
771 return self.squash_gerrit_uploads
772
tandrii@chromium.org28253532016-04-14 13:46:56 +0000773 def GetGerritSkipEnsureAuthenticated(self):
774 """Return True if EnsureAuthenticated should not be done for Gerrit
775 uploads."""
776 if self.gerrit_skip_ensure_authenticated is None:
777 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000778 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000779 error_ok=True).strip() == 'true')
780 return self.gerrit_skip_ensure_authenticated
781
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000782 def GetGitEditor(self):
783 """Return the editor specified in the git config, or None if none is."""
784 if self.git_editor is None:
785 self.git_editor = self._GetConfig('core.editor', error_ok=True)
786 return self.git_editor or None
787
thestig@chromium.org44202a22014-03-11 19:22:18 +0000788 def GetLintRegex(self):
789 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
790 DEFAULT_LINT_REGEX)
791
792 def GetLintIgnoreRegex(self):
793 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
794 DEFAULT_LINT_IGNORE_REGEX)
795
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000796 def GetProject(self):
797 if not self.project:
798 self.project = self._GetRietveldConfig('project', error_ok=True)
799 return self.project
800
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000801 def GetForceHttpsCommitUrl(self):
802 if not self.force_https_commit_url:
803 self.force_https_commit_url = self._GetRietveldConfig(
804 'force-https-commit-url', error_ok=True)
805 return self.force_https_commit_url
806
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000807 def GetPendingRefPrefix(self):
808 if not self.pending_ref_prefix:
809 self.pending_ref_prefix = self._GetRietveldConfig(
810 'pending-ref-prefix', error_ok=True)
811 return self.pending_ref_prefix
812
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000813 def _GetRietveldConfig(self, param, **kwargs):
814 return self._GetConfig('rietveld.' + param, **kwargs)
815
rmistry@google.com78948ed2015-07-08 23:09:57 +0000816 def _GetBranchConfig(self, branch_name, param, **kwargs):
817 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
818
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000819 def _GetConfig(self, param, **kwargs):
820 self.LazyUpdateIfNeeded()
821 return RunGit(['config', param], **kwargs).strip()
822
823
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000824def ShortBranchName(branch):
825 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000826 return branch.replace('refs/heads/', '', 1)
827
828
829def GetCurrentBranchRef():
830 """Returns branch ref (e.g., refs/heads/master) or None."""
831 return RunGit(['symbolic-ref', 'HEAD'],
832 stderr=subprocess2.VOID, error_ok=True).strip() or None
833
834
835def GetCurrentBranch():
836 """Returns current branch or None.
837
838 For refs/heads/* branches, returns just last part. For others, full ref.
839 """
840 branchref = GetCurrentBranchRef()
841 if branchref:
842 return ShortBranchName(branchref)
843 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000844
845
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000846class _CQState(object):
847 """Enum for states of CL with respect to Commit Queue."""
848 NONE = 'none'
849 DRY_RUN = 'dry_run'
850 COMMIT = 'commit'
851
852 ALL_STATES = [NONE, DRY_RUN, COMMIT]
853
854
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000855class _ParsedIssueNumberArgument(object):
856 def __init__(self, issue=None, patchset=None, hostname=None):
857 self.issue = issue
858 self.patchset = patchset
859 self.hostname = hostname
860
861 @property
862 def valid(self):
863 return self.issue is not None
864
865
866class _RietveldParsedIssueNumberArgument(_ParsedIssueNumberArgument):
867 def __init__(self, *args, **kwargs):
868 self.patch_url = kwargs.pop('patch_url', None)
869 super(_RietveldParsedIssueNumberArgument, self).__init__(*args, **kwargs)
870
871
872def ParseIssueNumberArgument(arg):
873 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
874 fail_result = _ParsedIssueNumberArgument()
875
876 if arg.isdigit():
877 return _ParsedIssueNumberArgument(issue=int(arg))
878 if not arg.startswith('http'):
879 return fail_result
880 url = gclient_utils.UpgradeToHttps(arg)
881 try:
882 parsed_url = urlparse.urlparse(url)
883 except ValueError:
884 return fail_result
885 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
886 tmp = cls.ParseIssueURL(parsed_url)
887 if tmp is not None:
888 return tmp
889 return fail_result
890
891
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000892class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000893 """Changelist works with one changelist in local branch.
894
895 Supports two codereview backends: Rietveld or Gerrit, selected at object
896 creation.
897
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +0000898 Notes:
899 * Not safe for concurrent multi-{thread,process} use.
900 * Caches values from current branch. Therefore, re-use after branch change
901 with care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000902 """
903
904 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
905 """Create a new ChangeList instance.
906
907 If issue is given, the codereview must be given too.
908
909 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
910 Otherwise, it's decided based on current configuration of the local branch,
911 with default being 'rietveld' for backwards compatibility.
912 See _load_codereview_impl for more details.
913
914 **kwargs will be passed directly to codereview implementation.
915 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000916 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000917 global settings
918 if not settings:
919 # Happens when git_cl.py is used as a utility library.
920 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000921
922 if issue:
923 assert codereview, 'codereview must be known, if issue is known'
924
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000925 self.branchref = branchref
926 if self.branchref:
927 self.branch = ShortBranchName(self.branchref)
928 else:
929 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000930 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000931 self.lookedup_issue = False
932 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000933 self.has_description = False
934 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000935 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000936 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000937 self.cc = None
938 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000939 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000940
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000941 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000942 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000943 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000944 assert self._codereview_impl
945 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000946
947 def _load_codereview_impl(self, codereview=None, **kwargs):
948 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000949 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
950 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
951 self._codereview = codereview
952 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000953 return
954
955 # Automatic selection based on issue number set for a current branch.
956 # Rietveld takes precedence over Gerrit.
957 assert not self.issue
958 # Whether we find issue or not, we are doing the lookup.
959 self.lookedup_issue = True
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000960 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000961 setting = cls.IssueSetting(self.GetBranch())
962 issue = RunGit(['config', setting], error_ok=True).strip()
963 if issue:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000964 self._codereview = codereview
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000965 self._codereview_impl = cls(self, **kwargs)
966 self.issue = int(issue)
967 return
968
969 # No issue is set for this branch, so decide based on repo-wide settings.
970 return self._load_codereview_impl(
971 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
972 **kwargs)
973
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000974 def IsGerrit(self):
975 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000976
977 def GetCCList(self):
978 """Return the users cc'd on this CL.
979
980 Return is a string suitable for passing to gcl with the --cc flag.
981 """
982 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000983 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000984 more_cc = ','.join(self.watchers)
985 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
986 return self.cc
987
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000988 def GetCCListWithoutDefault(self):
989 """Return the users cc'd on this CL excluding default ones."""
990 if self.cc is None:
991 self.cc = ','.join(self.watchers)
992 return self.cc
993
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000994 def SetWatchers(self, watchers):
995 """Set the list of email addresses that should be cc'd based on the changed
996 files in this CL.
997 """
998 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000999
1000 def GetBranch(self):
1001 """Returns the short branch name, e.g. 'master'."""
1002 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001003 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001004 if not branchref:
1005 return None
1006 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001007 self.branch = ShortBranchName(self.branchref)
1008 return self.branch
1009
1010 def GetBranchRef(self):
1011 """Returns the full branch name, e.g. 'refs/heads/master'."""
1012 self.GetBranch() # Poke the lazy loader.
1013 return self.branchref
1014
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001015 def ClearBranch(self):
1016 """Clears cached branch data of this object."""
1017 self.branch = self.branchref = None
1018
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001019 @staticmethod
1020 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001021 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001022 e.g. 'origin', 'refs/heads/master'
1023 """
1024 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001025 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
1026 error_ok=True).strip()
1027 if upstream_branch:
1028 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
1029 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001030 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1031 error_ok=True).strip()
1032 if upstream_branch:
1033 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001034 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001035 # Fall back on trying a git-svn upstream branch.
1036 if settings.GetIsGitSvn():
1037 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001038 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001039 # Else, try to guess the origin remote.
1040 remote_branches = RunGit(['branch', '-r']).split()
1041 if 'origin/master' in remote_branches:
1042 # Fall back on origin/master if it exits.
1043 remote = 'origin'
1044 upstream_branch = 'refs/heads/master'
1045 elif 'origin/trunk' in remote_branches:
1046 # Fall back on origin/trunk if it exists. Generally a shared
1047 # git-svn clone
1048 remote = 'origin'
1049 upstream_branch = 'refs/heads/trunk'
1050 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001051 DieWithError(
1052 'Unable to determine default branch to diff against.\n'
1053 'Either pass complete "git diff"-style arguments, like\n'
1054 ' git cl upload origin/master\n'
1055 'or verify this branch is set up to track another \n'
1056 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001057
1058 return remote, upstream_branch
1059
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001060 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001061 upstream_branch = self.GetUpstreamBranch()
1062 if not BranchExists(upstream_branch):
1063 DieWithError('The upstream for the current branch (%s) does not exist '
1064 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001065 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001066 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001067
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001068 def GetUpstreamBranch(self):
1069 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001070 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001071 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001072 upstream_branch = upstream_branch.replace('refs/heads/',
1073 'refs/remotes/%s/' % remote)
1074 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1075 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001076 self.upstream_branch = upstream_branch
1077 return self.upstream_branch
1078
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001079 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001080 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001081 remote, branch = None, self.GetBranch()
1082 seen_branches = set()
1083 while branch not in seen_branches:
1084 seen_branches.add(branch)
1085 remote, branch = self.FetchUpstreamTuple(branch)
1086 branch = ShortBranchName(branch)
1087 if remote != '.' or branch.startswith('refs/remotes'):
1088 break
1089 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001090 remotes = RunGit(['remote'], error_ok=True).split()
1091 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001092 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001093 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001094 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001095 logging.warning('Could not determine which remote this change is '
1096 'associated with, so defaulting to "%s". This may '
1097 'not be what you want. You may prevent this message '
1098 'by running "git svn info" as documented here: %s',
1099 self._remote,
1100 GIT_INSTRUCTIONS_URL)
1101 else:
1102 logging.warn('Could not determine which remote this change is '
1103 'associated with. You may prevent this message by '
1104 'running "git svn info" as documented here: %s',
1105 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001106 branch = 'HEAD'
1107 if branch.startswith('refs/remotes'):
1108 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001109 elif branch.startswith('refs/branch-heads/'):
1110 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001111 else:
1112 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001113 return self._remote
1114
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001115 def GitSanityChecks(self, upstream_git_obj):
1116 """Checks git repo status and ensures diff is from local commits."""
1117
sbc@chromium.org79706062015-01-14 21:18:12 +00001118 if upstream_git_obj is None:
1119 if self.GetBranch() is None:
1120 print >> sys.stderr, (
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00001121 'ERROR: unable to determine current branch (detached HEAD?)')
sbc@chromium.org79706062015-01-14 21:18:12 +00001122 else:
1123 print >> sys.stderr, (
1124 'ERROR: no upstream branch')
1125 return False
1126
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001127 # Verify the commit we're diffing against is in our current branch.
1128 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1129 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1130 if upstream_sha != common_ancestor:
1131 print >> sys.stderr, (
1132 'ERROR: %s is not in the current branch. You may need to rebase '
1133 'your tracking branch' % upstream_sha)
1134 return False
1135
1136 # List the commits inside the diff, and verify they are all local.
1137 commits_in_diff = RunGit(
1138 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1139 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1140 remote_branch = remote_branch.strip()
1141 if code != 0:
1142 _, remote_branch = self.GetRemoteBranch()
1143
1144 commits_in_remote = RunGit(
1145 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1146
1147 common_commits = set(commits_in_diff) & set(commits_in_remote)
1148 if common_commits:
1149 print >> sys.stderr, (
1150 'ERROR: Your diff contains %d commits already in %s.\n'
1151 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1152 'the diff. If you are using a custom git flow, you can override'
1153 ' the reference used for this check with "git config '
1154 'gitcl.remotebranch <git-ref>".' % (
1155 len(common_commits), remote_branch, upstream_git_obj))
1156 return False
1157 return True
1158
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001159 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001160 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001161
1162 Returns None if it is not set.
1163 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001164 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
1165 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001166
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001167 def GetGitSvnRemoteUrl(self):
1168 """Return the configured git-svn remote URL parsed from git svn info.
1169
1170 Returns None if it is not set.
1171 """
1172 # URL is dependent on the current directory.
1173 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1174 if data:
1175 keys = dict(line.split(': ', 1) for line in data.splitlines()
1176 if ': ' in line)
1177 return keys.get('URL', None)
1178 return None
1179
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001180 def GetRemoteUrl(self):
1181 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1182
1183 Returns None if there is no remote.
1184 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001185 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001186 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1187
1188 # If URL is pointing to a local directory, it is probably a git cache.
1189 if os.path.isdir(url):
1190 url = RunGit(['config', 'remote.%s.url' % remote],
1191 error_ok=True,
1192 cwd=url).strip()
1193 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001194
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001195 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001196 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001197 if self.issue is None and not self.lookedup_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001198 issue = RunGit(['config',
1199 self._codereview_impl.IssueSetting(self.GetBranch())],
1200 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001201 self.issue = int(issue) or None if issue else None
1202 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001203 return self.issue
1204
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001205 def GetIssueURL(self):
1206 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001207 issue = self.GetIssue()
1208 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001209 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001210 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001211
1212 def GetDescription(self, pretty=False):
1213 if not self.has_description:
1214 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001215 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001216 self.has_description = True
1217 if pretty:
1218 wrapper = textwrap.TextWrapper()
1219 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1220 return wrapper.fill(self.description)
1221 return self.description
1222
1223 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001224 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001225 if self.patchset is None and not self.lookedup_patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001226 patchset = RunGit(['config', self._codereview_impl.PatchsetSetting()],
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001227 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001228 self.patchset = int(patchset) or None if patchset else None
1229 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001230 return self.patchset
1231
1232 def SetPatchset(self, patchset):
1233 """Set this branch's patchset. If patchset=0, clears the patchset."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001234 patchset_setting = self._codereview_impl.PatchsetSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001235 if patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001236 RunGit(['config', patchset_setting, str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001237 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001238 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001239 RunGit(['config', '--unset', patchset_setting],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001240 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001241 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001242
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001243 def SetIssue(self, issue=None):
1244 """Set this branch's issue. If issue isn't given, clears the issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001245 issue_setting = self._codereview_impl.IssueSetting(self.GetBranch())
1246 codereview_setting = self._codereview_impl.GetCodereviewServerSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001247 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001248 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001249 RunGit(['config', issue_setting, str(issue)])
1250 codereview_server = self._codereview_impl.GetCodereviewServer()
1251 if codereview_server:
1252 RunGit(['config', codereview_setting, codereview_server])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001253 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +00001254 current_issue = self.GetIssue()
1255 if current_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001256 RunGit(['config', '--unset', issue_setting])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001257 self.issue = None
1258 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001259
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001260 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001261 if not self.GitSanityChecks(upstream_branch):
1262 DieWithError('\nGit sanity check failure')
1263
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001264 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001265 if not root:
1266 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001267 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001268
1269 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001270 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001271 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001272 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001273 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001274 except subprocess2.CalledProcessError:
1275 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001276 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001277 'This branch probably doesn\'t exist anymore. To reset the\n'
1278 'tracking branch, please run\n'
1279 ' git branch --set-upstream %s trunk\n'
1280 'replacing trunk with origin/master or the relevant branch') %
1281 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001282
maruel@chromium.org52424302012-08-29 15:14:30 +00001283 issue = self.GetIssue()
1284 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001285 if issue:
1286 description = self.GetDescription()
1287 else:
1288 # If the change was never uploaded, use the log messages of all commits
1289 # up to the branch point, as git cl upload will prefill the description
1290 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001291 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1292 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001293
1294 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001295 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001296 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001297 name,
1298 description,
1299 absroot,
1300 files,
1301 issue,
1302 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001303 author,
1304 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001305
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001306 def UpdateDescription(self, description):
1307 self.description = description
1308 return self._codereview_impl.UpdateDescriptionRemote(description)
1309
1310 def RunHook(self, committing, may_prompt, verbose, change):
1311 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1312 try:
1313 return presubmit_support.DoPresubmitChecks(change, committing,
1314 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1315 default_presubmit=None, may_prompt=may_prompt,
1316 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit())
1317 except presubmit_support.PresubmitFailure, e:
1318 DieWithError(
1319 ('%s\nMaybe your depot_tools is out of date?\n'
1320 'If all fails, contact maruel@') % e)
1321
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001322 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1323 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001324 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1325 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001326 else:
1327 # Assume url.
1328 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1329 urlparse.urlparse(issue_arg))
1330 if not parsed_issue_arg or not parsed_issue_arg.valid:
1331 DieWithError('Failed to parse issue argument "%s". '
1332 'Must be an issue number or a valid URL.' % issue_arg)
1333 return self._codereview_impl.CMDPatchWithParsedIssue(
1334 parsed_issue_arg, reject, nocommit, directory)
1335
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001336 def CMDUpload(self, options, git_diff_args, orig_args):
1337 """Uploads a change to codereview."""
1338 if git_diff_args:
1339 # TODO(ukai): is it ok for gerrit case?
1340 base_branch = git_diff_args[0]
1341 else:
1342 if self.GetBranch() is None:
1343 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1344
1345 # Default to diffing against common ancestor of upstream branch
1346 base_branch = self.GetCommonAncestorWithUpstream()
1347 git_diff_args = [base_branch, 'HEAD']
1348
1349 # Make sure authenticated to codereview before running potentially expensive
1350 # hooks. It is a fast, best efforts check. Codereview still can reject the
1351 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001352 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001353
1354 # Apply watchlists on upload.
1355 change = self.GetChange(base_branch, None)
1356 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1357 files = [f.LocalPath() for f in change.AffectedFiles()]
1358 if not options.bypass_watchlists:
1359 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1360
1361 if not options.bypass_hooks:
1362 if options.reviewers or options.tbr_owners:
1363 # Set the reviewer list now so that presubmit checks can access it.
1364 change_description = ChangeDescription(change.FullDescriptionText())
1365 change_description.update_reviewers(options.reviewers,
1366 options.tbr_owners,
1367 change)
1368 change.SetDescriptionText(change_description.description)
1369 hook_results = self.RunHook(committing=False,
1370 may_prompt=not options.force,
1371 verbose=options.verbose,
1372 change=change)
1373 if not hook_results.should_continue():
1374 return 1
1375 if not options.reviewers and hook_results.reviewers:
1376 options.reviewers = hook_results.reviewers.split(',')
1377
1378 if self.GetIssue():
1379 latest_patchset = self.GetMostRecentPatchset()
1380 local_patchset = self.GetPatchset()
1381 if (latest_patchset and local_patchset and
1382 local_patchset != latest_patchset):
1383 print ('The last upload made from this repository was patchset #%d but '
1384 'the most recent patchset on the server is #%d.'
1385 % (local_patchset, latest_patchset))
1386 print ('Uploading will still work, but if you\'ve uploaded to this '
1387 'issue from another machine or branch the patch you\'re '
1388 'uploading now might not include those changes.')
1389 ask_for_data('About to upload; enter to confirm.')
1390
1391 print_stats(options.similarity, options.find_copies, git_diff_args)
1392 ret = self.CMDUploadChange(options, git_diff_args, change)
1393 if not ret:
1394 git_set_branch_value('last-upload-hash',
1395 RunGit(['rev-parse', 'HEAD']).strip())
1396 # Run post upload hooks, if specified.
1397 if settings.GetRunPostUploadHook():
1398 presubmit_support.DoPostUploadExecuter(
1399 change,
1400 self,
1401 settings.GetRoot(),
1402 options.verbose,
1403 sys.stdout)
1404
1405 # Upload all dependencies if specified.
1406 if options.dependencies:
1407 print
1408 print '--dependencies has been specified.'
1409 print 'All dependent local branches will be re-uploaded.'
1410 print
1411 # Remove the dependencies flag from args so that we do not end up in a
1412 # loop.
1413 orig_args.remove('--dependencies')
1414 ret = upload_branch_deps(self, orig_args)
1415 return ret
1416
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001417 def SetCQState(self, new_state):
1418 """Update the CQ state for latest patchset.
1419
1420 Issue must have been already uploaded and known.
1421 """
1422 assert new_state in _CQState.ALL_STATES
1423 assert self.GetIssue()
1424 return self._codereview_impl.SetCQState(new_state)
1425
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001426 # Forward methods to codereview specific implementation.
1427
1428 def CloseIssue(self):
1429 return self._codereview_impl.CloseIssue()
1430
1431 def GetStatus(self):
1432 return self._codereview_impl.GetStatus()
1433
1434 def GetCodereviewServer(self):
1435 return self._codereview_impl.GetCodereviewServer()
1436
1437 def GetApprovingReviewers(self):
1438 return self._codereview_impl.GetApprovingReviewers()
1439
1440 def GetMostRecentPatchset(self):
1441 return self._codereview_impl.GetMostRecentPatchset()
1442
1443 def __getattr__(self, attr):
1444 # This is because lots of untested code accesses Rietveld-specific stuff
1445 # directly, and it's hard to fix for sure. So, just let it work, and fix
1446 # on a cases by case basis.
1447 return getattr(self._codereview_impl, attr)
1448
1449
1450class _ChangelistCodereviewBase(object):
1451 """Abstract base class encapsulating codereview specifics of a changelist."""
1452 def __init__(self, changelist):
1453 self._changelist = changelist # instance of Changelist
1454
1455 def __getattr__(self, attr):
1456 # Forward methods to changelist.
1457 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1458 # _RietveldChangelistImpl to avoid this hack?
1459 return getattr(self._changelist, attr)
1460
1461 def GetStatus(self):
1462 """Apply a rough heuristic to give a simple summary of an issue's review
1463 or CQ status, assuming adherence to a common workflow.
1464
1465 Returns None if no issue for this branch, or specific string keywords.
1466 """
1467 raise NotImplementedError()
1468
1469 def GetCodereviewServer(self):
1470 """Returns server URL without end slash, like "https://codereview.com"."""
1471 raise NotImplementedError()
1472
1473 def FetchDescription(self):
1474 """Fetches and returns description from the codereview server."""
1475 raise NotImplementedError()
1476
1477 def GetCodereviewServerSetting(self):
1478 """Returns git config setting for the codereview server."""
1479 raise NotImplementedError()
1480
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001481 @classmethod
1482 def IssueSetting(cls, branch):
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001483 return 'branch.%s.%s' % (branch, cls.IssueSettingSuffix())
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001484
1485 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001486 def IssueSettingSuffix(cls):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001487 """Returns name of git config setting which stores issue number for a given
1488 branch."""
1489 raise NotImplementedError()
1490
1491 def PatchsetSetting(self):
1492 """Returns name of git config setting which stores issue number."""
1493 raise NotImplementedError()
1494
1495 def GetRieveldObjForPresubmit(self):
1496 # This is an unfortunate Rietveld-embeddedness in presubmit.
1497 # For non-Rietveld codereviews, this probably should return a dummy object.
1498 raise NotImplementedError()
1499
1500 def UpdateDescriptionRemote(self, description):
1501 """Update the description on codereview site."""
1502 raise NotImplementedError()
1503
1504 def CloseIssue(self):
1505 """Closes the issue."""
1506 raise NotImplementedError()
1507
1508 def GetApprovingReviewers(self):
1509 """Returns a list of reviewers approving the change.
1510
1511 Note: not necessarily committers.
1512 """
1513 raise NotImplementedError()
1514
1515 def GetMostRecentPatchset(self):
1516 """Returns the most recent patchset number from the codereview site."""
1517 raise NotImplementedError()
1518
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001519 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1520 directory):
1521 """Fetches and applies the issue.
1522
1523 Arguments:
1524 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1525 reject: if True, reject the failed patch instead of switching to 3-way
1526 merge. Rietveld only.
1527 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1528 only.
1529 directory: switch to directory before applying the patch. Rietveld only.
1530 """
1531 raise NotImplementedError()
1532
1533 @staticmethod
1534 def ParseIssueURL(parsed_url):
1535 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1536 failed."""
1537 raise NotImplementedError()
1538
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001539 def EnsureAuthenticated(self, force):
1540 """Best effort check that user is authenticated with codereview server.
1541
1542 Arguments:
1543 force: whether to skip confirmation questions.
1544 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001545 raise NotImplementedError()
1546
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001547 def CMDUploadChange(self, options, args, change):
1548 """Uploads a change to codereview."""
1549 raise NotImplementedError()
1550
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001551 def SetCQState(self, new_state):
1552 """Update the CQ state for latest patchset.
1553
1554 Issue must have been already uploaded and known.
1555 """
1556 raise NotImplementedError()
1557
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001558
1559class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1560 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1561 super(_RietveldChangelistImpl, self).__init__(changelist)
1562 assert settings, 'must be initialized in _ChangelistCodereviewBase'
1563 settings.GetDefaultServerUrl()
1564
1565 self._rietveld_server = rietveld_server
1566 self._auth_config = auth_config
1567 self._props = None
1568 self._rpc_server = None
1569
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001570 def GetCodereviewServer(self):
1571 if not self._rietveld_server:
1572 # If we're on a branch then get the server potentially associated
1573 # with that branch.
1574 if self.GetIssue():
1575 rietveld_server_setting = self.GetCodereviewServerSetting()
1576 if rietveld_server_setting:
1577 self._rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1578 ['config', rietveld_server_setting], error_ok=True).strip())
1579 if not self._rietveld_server:
1580 self._rietveld_server = settings.GetDefaultServerUrl()
1581 return self._rietveld_server
1582
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001583 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001584 """Best effort check that user is authenticated with Rietveld server."""
1585 if self._auth_config.use_oauth2:
1586 authenticator = auth.get_authenticator_for_host(
1587 self.GetCodereviewServer(), self._auth_config)
1588 if not authenticator.has_cached_credentials():
1589 raise auth.LoginRequiredError(self.GetCodereviewServer())
1590
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001591 def FetchDescription(self):
1592 issue = self.GetIssue()
1593 assert issue
1594 try:
1595 return self.RpcServer().get_description(issue).strip()
1596 except urllib2.HTTPError as e:
1597 if e.code == 404:
1598 DieWithError(
1599 ('\nWhile fetching the description for issue %d, received a '
1600 '404 (not found)\n'
1601 'error. It is likely that you deleted this '
1602 'issue on the server. If this is the\n'
1603 'case, please run\n\n'
1604 ' git cl issue 0\n\n'
1605 'to clear the association with the deleted issue. Then run '
1606 'this command again.') % issue)
1607 else:
1608 DieWithError(
1609 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1610 except urllib2.URLError as e:
1611 print >> sys.stderr, (
1612 'Warning: Failed to retrieve CL description due to network '
1613 'failure.')
1614 return ''
1615
1616 def GetMostRecentPatchset(self):
1617 return self.GetIssueProperties()['patchsets'][-1]
1618
1619 def GetPatchSetDiff(self, issue, patchset):
1620 return self.RpcServer().get(
1621 '/download/issue%s_%s.diff' % (issue, patchset))
1622
1623 def GetIssueProperties(self):
1624 if self._props is None:
1625 issue = self.GetIssue()
1626 if not issue:
1627 self._props = {}
1628 else:
1629 self._props = self.RpcServer().get_issue_properties(issue, True)
1630 return self._props
1631
1632 def GetApprovingReviewers(self):
1633 return get_approving_reviewers(self.GetIssueProperties())
1634
1635 def AddComment(self, message):
1636 return self.RpcServer().add_comment(self.GetIssue(), message)
1637
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001638 def GetStatus(self):
1639 """Apply a rough heuristic to give a simple summary of an issue's review
1640 or CQ status, assuming adherence to a common workflow.
1641
1642 Returns None if no issue for this branch, or one of the following keywords:
1643 * 'error' - error from review tool (including deleted issues)
1644 * 'unsent' - not sent for review
1645 * 'waiting' - waiting for review
1646 * 'reply' - waiting for owner to reply to review
1647 * 'lgtm' - LGTM from at least one approved reviewer
1648 * 'commit' - in the commit queue
1649 * 'closed' - closed
1650 """
1651 if not self.GetIssue():
1652 return None
1653
1654 try:
1655 props = self.GetIssueProperties()
1656 except urllib2.HTTPError:
1657 return 'error'
1658
1659 if props.get('closed'):
1660 # Issue is closed.
1661 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001662 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001663 # Issue is in the commit queue.
1664 return 'commit'
1665
1666 try:
1667 reviewers = self.GetApprovingReviewers()
1668 except urllib2.HTTPError:
1669 return 'error'
1670
1671 if reviewers:
1672 # Was LGTM'ed.
1673 return 'lgtm'
1674
1675 messages = props.get('messages') or []
1676
1677 if not messages:
1678 # No message was sent.
1679 return 'unsent'
1680 if messages[-1]['sender'] != props.get('owner_email'):
1681 # Non-LGTM reply from non-owner
1682 return 'reply'
1683 return 'waiting'
1684
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001685 def UpdateDescriptionRemote(self, description):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001686 return self.RpcServer().update_description(
1687 self.GetIssue(), self.description)
1688
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001689 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001690 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001691
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001692 def SetFlag(self, flag, value):
1693 """Patchset must match."""
1694 if not self.GetPatchset():
1695 DieWithError('The patchset needs to match. Send another patchset.')
1696 try:
1697 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001698 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001699 except urllib2.HTTPError, e:
1700 if e.code == 404:
1701 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1702 if e.code == 403:
1703 DieWithError(
1704 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1705 'match?') % (self.GetIssue(), self.GetPatchset()))
1706 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001707
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001708 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001709 """Returns an upload.RpcServer() to access this review's rietveld instance.
1710 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001711 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001712 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001713 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001714 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001715 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001716
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001717 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001718 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001719 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001720
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001721 def PatchsetSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001722 """Return the git setting that stores this change's most recent patchset."""
1723 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1724
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001725 def GetCodereviewServerSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001726 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001727 branch = self.GetBranch()
1728 if branch:
1729 return 'branch.%s.rietveldserver' % branch
1730 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001731
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001732 def GetRieveldObjForPresubmit(self):
1733 return self.RpcServer()
1734
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001735 def SetCQState(self, new_state):
1736 props = self.GetIssueProperties()
1737 if props.get('private'):
1738 DieWithError('Cannot set-commit on private issue')
1739
1740 if new_state == _CQState.COMMIT:
1741 self.SetFlag('commit', '1')
1742 elif new_state == _CQState.NONE:
1743 self.SetFlag('commit', '0')
1744 else:
1745 raise NotImplementedError()
1746
1747
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001748 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1749 directory):
1750 # TODO(maruel): Use apply_issue.py
1751
1752 # PatchIssue should never be called with a dirty tree. It is up to the
1753 # caller to check this, but just in case we assert here since the
1754 # consequences of the caller not checking this could be dire.
1755 assert(not git_common.is_dirty_git_tree('apply'))
1756 assert(parsed_issue_arg.valid)
1757 self._changelist.issue = parsed_issue_arg.issue
1758 if parsed_issue_arg.hostname:
1759 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1760
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001761 if (isinstance(parsed_issue_arg, _RietveldParsedIssueNumberArgument) and
1762 parsed_issue_arg.patch_url):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001763 assert parsed_issue_arg.patchset
1764 patchset = parsed_issue_arg.patchset
1765 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1766 else:
1767 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1768 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1769
1770 # Switch up to the top-level directory, if necessary, in preparation for
1771 # applying the patch.
1772 top = settings.GetRelativeRoot()
1773 if top:
1774 os.chdir(top)
1775
1776 # Git patches have a/ at the beginning of source paths. We strip that out
1777 # with a sed script rather than the -p flag to patch so we can feed either
1778 # Git or svn-style patches into the same apply command.
1779 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1780 try:
1781 patch_data = subprocess2.check_output(
1782 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1783 except subprocess2.CalledProcessError:
1784 DieWithError('Git patch mungling failed.')
1785 logging.info(patch_data)
1786
1787 # We use "git apply" to apply the patch instead of "patch" so that we can
1788 # pick up file adds.
1789 # The --index flag means: also insert into the index (so we catch adds).
1790 cmd = ['git', 'apply', '--index', '-p0']
1791 if directory:
1792 cmd.extend(('--directory', directory))
1793 if reject:
1794 cmd.append('--reject')
1795 elif IsGitVersionAtLeast('1.7.12'):
1796 cmd.append('--3way')
1797 try:
1798 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
1799 stdin=patch_data, stdout=subprocess2.VOID)
1800 except subprocess2.CalledProcessError:
1801 print 'Failed to apply the patch'
1802 return 1
1803
1804 # If we had an issue, commit the current state and register the issue.
1805 if not nocommit:
1806 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
1807 'patch from issue %(i)s at patchset '
1808 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
1809 % {'i': self.GetIssue(), 'p': patchset})])
1810 self.SetIssue(self.GetIssue())
1811 self.SetPatchset(patchset)
1812 print "Committed patch locally."
1813 else:
1814 print "Patch applied to index."
1815 return 0
1816
1817 @staticmethod
1818 def ParseIssueURL(parsed_url):
1819 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1820 return None
1821 # Typical url: https://domain/<issue_number>[/[other]]
1822 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
1823 if match:
1824 return _RietveldParsedIssueNumberArgument(
1825 issue=int(match.group(1)),
1826 hostname=parsed_url.netloc)
1827 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
1828 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
1829 if match:
1830 return _RietveldParsedIssueNumberArgument(
1831 issue=int(match.group(1)),
1832 patchset=int(match.group(2)),
1833 hostname=parsed_url.netloc,
1834 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
1835 return None
1836
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001837 def CMDUploadChange(self, options, args, change):
1838 """Upload the patch to Rietveld."""
1839 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1840 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001841 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
1842 if options.emulate_svn_auto_props:
1843 upload_args.append('--emulate_svn_auto_props')
1844
1845 change_desc = None
1846
1847 if options.email is not None:
1848 upload_args.extend(['--email', options.email])
1849
1850 if self.GetIssue():
1851 if options.title:
1852 upload_args.extend(['--title', options.title])
1853 if options.message:
1854 upload_args.extend(['--message', options.message])
1855 upload_args.extend(['--issue', str(self.GetIssue())])
1856 print ('This branch is associated with issue %s. '
1857 'Adding patch to that issue.' % self.GetIssue())
1858 else:
1859 if options.title:
1860 upload_args.extend(['--title', options.title])
1861 message = (options.title or options.message or
1862 CreateDescriptionFromLog(args))
1863 change_desc = ChangeDescription(message)
1864 if options.reviewers or options.tbr_owners:
1865 change_desc.update_reviewers(options.reviewers,
1866 options.tbr_owners,
1867 change)
1868 if not options.force:
1869 change_desc.prompt()
1870
1871 if not change_desc.description:
1872 print "Description is empty; aborting."
1873 return 1
1874
1875 upload_args.extend(['--message', change_desc.description])
1876 if change_desc.get_reviewers():
1877 upload_args.append('--reviewers=%s' % ','.join(
1878 change_desc.get_reviewers()))
1879 if options.send_mail:
1880 if not change_desc.get_reviewers():
1881 DieWithError("Must specify reviewers to send email.")
1882 upload_args.append('--send_mail')
1883
1884 # We check this before applying rietveld.private assuming that in
1885 # rietveld.cc only addresses which we can send private CLs to are listed
1886 # if rietveld.private is set, and so we should ignore rietveld.cc only
1887 # when --private is specified explicitly on the command line.
1888 if options.private:
1889 logging.warn('rietveld.cc is ignored since private flag is specified. '
1890 'You need to review and add them manually if necessary.')
1891 cc = self.GetCCListWithoutDefault()
1892 else:
1893 cc = self.GetCCList()
1894 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
1895 if cc:
1896 upload_args.extend(['--cc', cc])
1897
1898 if options.private or settings.GetDefaultPrivateFlag() == "True":
1899 upload_args.append('--private')
1900
1901 upload_args.extend(['--git_similarity', str(options.similarity)])
1902 if not options.find_copies:
1903 upload_args.extend(['--git_no_find_copies'])
1904
1905 # Include the upstream repo's URL in the change -- this is useful for
1906 # projects that have their source spread across multiple repos.
1907 remote_url = self.GetGitBaseUrlFromConfig()
1908 if not remote_url:
1909 if settings.GetIsGitSvn():
1910 remote_url = self.GetGitSvnRemoteUrl()
1911 else:
1912 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
1913 remote_url = '%s@%s' % (self.GetRemoteUrl(),
1914 self.GetUpstreamBranch().split('/')[-1])
1915 if remote_url:
1916 upload_args.extend(['--base_url', remote_url])
1917 remote, remote_branch = self.GetRemoteBranch()
1918 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
1919 settings.GetPendingRefPrefix())
1920 if target_ref:
1921 upload_args.extend(['--target_ref', target_ref])
1922
1923 # Look for dependent patchsets. See crbug.com/480453 for more details.
1924 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
1925 upstream_branch = ShortBranchName(upstream_branch)
1926 if remote is '.':
1927 # A local branch is being tracked.
1928 local_branch = ShortBranchName(upstream_branch)
1929 if settings.GetIsSkipDependencyUpload(local_branch):
1930 print
1931 print ('Skipping dependency patchset upload because git config '
1932 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
1933 print
1934 else:
1935 auth_config = auth.extract_auth_config_from_options(options)
1936 branch_cl = Changelist(branchref=local_branch,
1937 auth_config=auth_config)
1938 branch_cl_issue_url = branch_cl.GetIssueURL()
1939 branch_cl_issue = branch_cl.GetIssue()
1940 branch_cl_patchset = branch_cl.GetPatchset()
1941 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
1942 upload_args.extend(
1943 ['--depends_on_patchset', '%s:%s' % (
1944 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001945 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001946 '\n'
1947 'The current branch (%s) is tracking a local branch (%s) with '
1948 'an associated CL.\n'
1949 'Adding %s/#ps%s as a dependency patchset.\n'
1950 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
1951 branch_cl_patchset))
1952
1953 project = settings.GetProject()
1954 if project:
1955 upload_args.extend(['--project', project])
1956
1957 if options.cq_dry_run:
1958 upload_args.extend(['--cq_dry_run'])
1959
1960 try:
1961 upload_args = ['upload'] + upload_args + args
1962 logging.info('upload.RealMain(%s)', upload_args)
1963 issue, patchset = upload.RealMain(upload_args)
1964 issue = int(issue)
1965 patchset = int(patchset)
1966 except KeyboardInterrupt:
1967 sys.exit(1)
1968 except:
1969 # If we got an exception after the user typed a description for their
1970 # change, back up the description before re-raising.
1971 if change_desc:
1972 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1973 print('\nGot exception while uploading -- saving description to %s\n' %
1974 backup_path)
1975 backup_file = open(backup_path, 'w')
1976 backup_file.write(change_desc.description)
1977 backup_file.close()
1978 raise
1979
1980 if not self.GetIssue():
1981 self.SetIssue(issue)
1982 self.SetPatchset(patchset)
1983
1984 if options.use_commit_queue:
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001985 self.SetCQState(_CQState.COMMIT)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001986 return 0
1987
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001988
1989class _GerritChangelistImpl(_ChangelistCodereviewBase):
1990 def __init__(self, changelist, auth_config=None):
1991 # auth_config is Rietveld thing, kept here to preserve interface only.
1992 super(_GerritChangelistImpl, self).__init__(changelist)
1993 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001994 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001995 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001996 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001997
1998 def _GetGerritHost(self):
1999 # Lazy load of configs.
2000 self.GetCodereviewServer()
2001 return self._gerrit_host
2002
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002003 def _GetGitHost(self):
2004 """Returns git host to be used when uploading change to Gerrit."""
2005 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2006
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002007 def GetCodereviewServer(self):
2008 if not self._gerrit_server:
2009 # If we're on a branch then get the server potentially associated
2010 # with that branch.
2011 if self.GetIssue():
2012 gerrit_server_setting = self.GetCodereviewServerSetting()
2013 if gerrit_server_setting:
2014 self._gerrit_server = RunGit(['config', gerrit_server_setting],
2015 error_ok=True).strip()
2016 if self._gerrit_server:
2017 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
2018 if not self._gerrit_server:
2019 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2020 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002021 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002022 parts[0] = parts[0] + '-review'
2023 self._gerrit_host = '.'.join(parts)
2024 self._gerrit_server = 'https://%s' % self._gerrit_host
2025 return self._gerrit_server
2026
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002027 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00002028 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002029 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002030
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002031 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002032 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002033 if settings.GetGerritSkipEnsureAuthenticated():
2034 # For projects with unusual authentication schemes.
2035 # See http://crbug.com/603378.
2036 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002037 # Lazy-loader to identify Gerrit and Git hosts.
2038 if gerrit_util.GceAuthenticator.is_gce():
2039 return
2040 self.GetCodereviewServer()
2041 git_host = self._GetGitHost()
2042 assert self._gerrit_server and self._gerrit_host
2043 cookie_auth = gerrit_util.CookiesAuthenticator()
2044
2045 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2046 git_auth = cookie_auth.get_auth_header(git_host)
2047 if gerrit_auth and git_auth:
2048 if gerrit_auth == git_auth:
2049 return
2050 print((
2051 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2052 ' Check your %s or %s file for credentials of hosts:\n'
2053 ' %s\n'
2054 ' %s\n'
2055 ' %s') %
2056 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2057 git_host, self._gerrit_host,
2058 cookie_auth.get_new_password_message(git_host)))
2059 if not force:
2060 ask_for_data('If you know what you are doing, press Enter to continue, '
2061 'Ctrl+C to abort.')
2062 return
2063 else:
2064 missing = (
2065 [] if gerrit_auth else [self._gerrit_host] +
2066 [] if git_auth else [git_host])
2067 DieWithError('Credentials for the following hosts are required:\n'
2068 ' %s\n'
2069 'These are read from %s (or legacy %s)\n'
2070 '%s' % (
2071 '\n '.join(missing),
2072 cookie_auth.get_gitcookies_path(),
2073 cookie_auth.get_netrc_path(),
2074 cookie_auth.get_new_password_message(git_host)))
2075
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002076
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002077 def PatchsetSetting(self):
2078 """Return the git setting that stores this change's most recent patchset."""
2079 return 'branch.%s.gerritpatchset' % self.GetBranch()
2080
2081 def GetCodereviewServerSetting(self):
2082 """Returns the git setting that stores this change's Gerrit server."""
2083 branch = self.GetBranch()
2084 if branch:
2085 return 'branch.%s.gerritserver' % branch
2086 return None
2087
2088 def GetRieveldObjForPresubmit(self):
2089 class ThisIsNotRietveldIssue(object):
2090 def __nonzero__(self):
2091 # This is a hack to make presubmit_support think that rietveld is not
2092 # defined, yet still ensure that calls directly result in a decent
2093 # exception message below.
2094 return False
2095
2096 def __getattr__(self, attr):
2097 print(
2098 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2099 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2100 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2101 'or use Rietveld for codereview.\n'
2102 'See also http://crbug.com/579160.' % attr)
2103 raise NotImplementedError()
2104 return ThisIsNotRietveldIssue()
2105
2106 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002107 """Apply a rough heuristic to give a simple summary of an issue's review
2108 or CQ status, assuming adherence to a common workflow.
2109
2110 Returns None if no issue for this branch, or one of the following keywords:
2111 * 'error' - error from review tool (including deleted issues)
2112 * 'unsent' - no reviewers added
2113 * 'waiting' - waiting for review
2114 * 'reply' - waiting for owner to reply to review
2115 * 'not lgtm' - Code-Review -2 from at least one approved reviewer
2116 * 'lgtm' - Code-Review +2 from at least one approved reviewer
2117 * 'commit' - in the commit queue
2118 * 'closed' - abandoned
2119 """
2120 if not self.GetIssue():
2121 return None
2122
2123 try:
2124 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
2125 except httplib.HTTPException:
2126 return 'error'
2127
2128 if data['status'] == 'ABANDONED':
2129 return 'closed'
2130
2131 cq_label = data['labels'].get('Commit-Queue', {})
2132 if cq_label:
2133 # Vote value is a stringified integer, which we expect from 0 to 2.
2134 vote_value = cq_label.get('value', '0')
2135 vote_text = cq_label.get('values', {}).get(vote_value, '')
2136 if vote_text.lower() == 'commit':
2137 return 'commit'
2138
2139 lgtm_label = data['labels'].get('Code-Review', {})
2140 if lgtm_label:
2141 if 'rejected' in lgtm_label:
2142 return 'not lgtm'
2143 if 'approved' in lgtm_label:
2144 return 'lgtm'
2145
2146 if not data.get('reviewers', {}).get('REVIEWER', []):
2147 return 'unsent'
2148
2149 messages = data.get('messages', [])
2150 if messages:
2151 owner = data['owner'].get('_account_id')
2152 last_message_author = messages[-1].get('author', {}).get('_account_id')
2153 if owner != last_message_author:
2154 # Some reply from non-owner.
2155 return 'reply'
2156
2157 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002158
2159 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002160 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002161 return data['revisions'][data['current_revision']]['_number']
2162
2163 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002164 data = self._GetChangeDetail(['CURRENT_REVISION'])
2165 current_rev = data['current_revision']
2166 url = data['revisions'][current_rev]['fetch']['http']['url']
2167 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002168
2169 def UpdateDescriptionRemote(self, description):
2170 # TODO(tandrii)
2171 raise NotImplementedError()
2172
2173 def CloseIssue(self):
2174 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2175
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002176 def SubmitIssue(self, wait_for_merge=True):
2177 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2178 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002179
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002180 def _GetChangeDetail(self, options=None, issue=None):
2181 options = options or []
2182 issue = issue or self.GetIssue()
2183 assert issue, 'issue required to query Gerrit'
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002184 return gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2185 options)
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002186
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002187 def CMDLand(self, force, bypass_hooks, verbose):
2188 if git_common.is_dirty_git_tree('land'):
2189 return 1
2190 differs = True
2191 last_upload = RunGit(['config',
2192 'branch.%s.gerritsquashhash' % self.GetBranch()],
2193 error_ok=True).strip()
2194 # Note: git diff outputs nothing if there is no diff.
2195 if not last_upload or RunGit(['diff', last_upload]).strip():
2196 print('WARNING: some changes from local branch haven\'t been uploaded')
2197 else:
2198 detail = self._GetChangeDetail(['CURRENT_REVISION'])
2199 if detail['current_revision'] == last_upload:
2200 differs = False
2201 else:
2202 print('WARNING: local branch contents differ from latest uploaded '
2203 'patchset')
2204 if differs:
2205 if not force:
2206 ask_for_data(
2207 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2208 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2209 elif not bypass_hooks:
2210 hook_results = self.RunHook(
2211 committing=True,
2212 may_prompt=not force,
2213 verbose=verbose,
2214 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2215 if not hook_results.should_continue():
2216 return 1
2217
2218 self.SubmitIssue(wait_for_merge=True)
2219 print('Issue %s has been submitted.' % self.GetIssueURL())
2220 return 0
2221
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002222 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2223 directory):
2224 assert not reject
2225 assert not nocommit
2226 assert not directory
2227 assert parsed_issue_arg.valid
2228
2229 self._changelist.issue = parsed_issue_arg.issue
2230
2231 if parsed_issue_arg.hostname:
2232 self._gerrit_host = parsed_issue_arg.hostname
2233 self._gerrit_server = 'https://%s' % self._gerrit_host
2234
2235 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2236
2237 if not parsed_issue_arg.patchset:
2238 # Use current revision by default.
2239 revision_info = detail['revisions'][detail['current_revision']]
2240 patchset = int(revision_info['_number'])
2241 else:
2242 patchset = parsed_issue_arg.patchset
2243 for revision_info in detail['revisions'].itervalues():
2244 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2245 break
2246 else:
2247 DieWithError('Couldn\'t find patchset %i in issue %i' %
2248 (parsed_issue_arg.patchset, self.GetIssue()))
2249
2250 fetch_info = revision_info['fetch']['http']
2251 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2252 RunGit(['cherry-pick', 'FETCH_HEAD'])
2253 self.SetIssue(self.GetIssue())
2254 self.SetPatchset(patchset)
2255 print('Committed patch for issue %i pathset %i locally' %
2256 (self.GetIssue(), self.GetPatchset()))
2257 return 0
2258
2259 @staticmethod
2260 def ParseIssueURL(parsed_url):
2261 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2262 return None
2263 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2264 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2265 # Short urls like https://domain/<issue_number> can be used, but don't allow
2266 # specifying the patchset (you'd 404), but we allow that here.
2267 if parsed_url.path == '/':
2268 part = parsed_url.fragment
2269 else:
2270 part = parsed_url.path
2271 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2272 if match:
2273 return _ParsedIssueNumberArgument(
2274 issue=int(match.group(2)),
2275 patchset=int(match.group(4)) if match.group(4) else None,
2276 hostname=parsed_url.netloc)
2277 return None
2278
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002279 def CMDUploadChange(self, options, args, change):
2280 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002281 if options.squash and options.no_squash:
2282 DieWithError('Can only use one of --squash or --no-squash')
2283 options.squash = ((settings.GetSquashGerritUploads() or options.squash) and
2284 not options.no_squash)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002285 # We assume the remote called "origin" is the one we want.
2286 # It is probably not worthwhile to support different workflows.
2287 gerrit_remote = 'origin'
2288
2289 remote, remote_branch = self.GetRemoteBranch()
2290 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2291 pending_prefix='')
2292
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002293 if options.squash:
2294 if not self.GetIssue():
2295 # TODO(tandrii): deperecate this after 2016Q2. Backwards compatibility
2296 # with shadow branch, which used to contain change-id for a given
2297 # branch, using which we can fetch actual issue number and set it as the
2298 # property of the branch, which is the new way.
2299 message = RunGitSilent([
2300 'show', '--format=%B', '-s',
2301 'refs/heads/git_cl_uploads/%s' % self.GetBranch()])
2302 if message:
2303 change_ids = git_footers.get_footer_change_id(message.strip())
2304 if change_ids and len(change_ids) == 1:
2305 details = self._GetChangeDetail(issue=change_ids[0])
2306 if details:
2307 print('WARNING: found old upload in branch git_cl_uploads/%s '
2308 'corresponding to issue %s' %
2309 (self.GetBranch(), details['_number']))
2310 self.SetIssue(details['_number'])
2311 if not self.GetIssue():
2312 DieWithError(
2313 '\n' # For readability of the blob below.
2314 'Found old upload in branch git_cl_uploads/%s, '
2315 'but failed to find corresponding Gerrit issue.\n'
2316 'If you know the issue number, set it manually first:\n'
2317 ' git cl issue 123456\n'
2318 'If you intended to upload this CL as new issue, '
2319 'just delete or rename the old upload branch:\n'
2320 ' git rename-branch git_cl_uploads/%s old_upload-%s\n'
2321 'After that, please run git cl upload again.' %
2322 tuple([self.GetBranch()] * 3))
2323 # End of backwards compatability.
2324
2325 if self.GetIssue():
2326 # Try to get the message from a previous upload.
2327 message = self.GetDescription()
2328 if not message:
2329 DieWithError(
2330 'failed to fetch description from current Gerrit issue %d\n'
2331 '%s' % (self.GetIssue(), self.GetIssueURL()))
2332 change_id = self._GetChangeDetail()['change_id']
2333 while True:
2334 footer_change_ids = git_footers.get_footer_change_id(message)
2335 if footer_change_ids == [change_id]:
2336 break
2337 if not footer_change_ids:
2338 message = git_footers.add_footer_change_id(message, change_id)
2339 print('WARNING: appended missing Change-Id to issue description')
2340 continue
2341 # There is already a valid footer but with different or several ids.
2342 # Doing this automatically is non-trivial as we don't want to lose
2343 # existing other footers, yet we want to append just 1 desired
2344 # Change-Id. Thus, just create a new footer, but let user verify the
2345 # new description.
2346 message = '%s\n\nChange-Id: %s' % (message, change_id)
2347 print(
2348 'WARNING: issue %s has Change-Id footer(s):\n'
2349 ' %s\n'
2350 'but issue has Change-Id %s, according to Gerrit.\n'
2351 'Please, check the proposed correction to the description, '
2352 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2353 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2354 change_id))
2355 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2356 if not options.force:
2357 change_desc = ChangeDescription(message)
2358 change_desc.prompt()
2359 message = change_desc.description
2360 if not message:
2361 DieWithError("Description is empty. Aborting...")
2362 # Continue the while loop.
2363 # Sanity check of this code - we should end up with proper message
2364 # footer.
2365 assert [change_id] == git_footers.get_footer_change_id(message)
2366 change_desc = ChangeDescription(message)
2367 else:
2368 change_desc = ChangeDescription(
2369 options.message or CreateDescriptionFromLog(args))
2370 if not options.force:
2371 change_desc.prompt()
2372 if not change_desc.description:
2373 DieWithError("Description is empty. Aborting...")
2374 message = change_desc.description
2375 change_ids = git_footers.get_footer_change_id(message)
2376 if len(change_ids) > 1:
2377 DieWithError('too many Change-Id footers, at most 1 allowed.')
2378 if not change_ids:
2379 # Generate the Change-Id automatically.
2380 message = git_footers.add_footer_change_id(
2381 message, GenerateGerritChangeId(message))
2382 change_desc.set_description(message)
2383 change_ids = git_footers.get_footer_change_id(message)
2384 assert len(change_ids) == 1
2385 change_id = change_ids[0]
2386
2387 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2388 if remote is '.':
2389 # If our upstream branch is local, we base our squashed commit on its
2390 # squashed version.
2391 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2392 # Check the squashed hash of the parent.
2393 parent = RunGit(['config',
2394 'branch.%s.gerritsquashhash' % upstream_branch_name],
2395 error_ok=True).strip()
2396 # Verify that the upstream branch has been uploaded too, otherwise
2397 # Gerrit will create additional CLs when uploading.
2398 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2399 RunGitSilent(['rev-parse', parent + ':'])):
2400 # TODO(tandrii): remove "old depot_tools" part on April 12, 2016.
2401 DieWithError(
2402 'Upload upstream branch %s first.\n'
2403 'Note: maybe you\'ve uploaded it with --no-squash or with an old '
2404 'version of depot_tools. If so, then re-upload it with:\n'
2405 ' git cl upload --squash\n' % upstream_branch_name)
2406 else:
2407 parent = self.GetCommonAncestorWithUpstream()
2408
2409 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2410 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2411 '-m', message]).strip()
2412 else:
2413 change_desc = ChangeDescription(
2414 options.message or CreateDescriptionFromLog(args))
2415 if not change_desc.description:
2416 DieWithError("Description is empty. Aborting...")
2417
2418 if not git_footers.get_footer_change_id(change_desc.description):
2419 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002420 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2421 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002422 ref_to_push = 'HEAD'
2423 parent = '%s/%s' % (gerrit_remote, branch)
2424 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2425
2426 assert change_desc
2427 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2428 ref_to_push)]).splitlines()
2429 if len(commits) > 1:
2430 print('WARNING: This will upload %d commits. Run the following command '
2431 'to see which commits will be uploaded: ' % len(commits))
2432 print('git log %s..%s' % (parent, ref_to_push))
2433 print('You can also use `git squash-branch` to squash these into a '
2434 'single commit.')
2435 ask_for_data('About to upload; enter to confirm.')
2436
2437 if options.reviewers or options.tbr_owners:
2438 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2439 change)
2440
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002441 # Extra options that can be specified at push time. Doc:
2442 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2443 refspec_opts = []
2444 if options.title:
2445 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2446 # reverse on its side.
2447 if '_' in options.title:
2448 print('WARNING: underscores in title will be converted to spaces.')
2449 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2450
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002451 cc = self.GetCCList().split(',')
2452 if options.cc:
2453 cc.extend(options.cc)
2454 cc = filter(None, cc)
2455 if cc:
tandrii@chromium.org0b2d7072016-04-18 16:19:03 +00002456 # refspec_opts.extend('cc=' + email.strip() for email in cc)
2457 # TODO(tandrii): enable this back. http://crbug.com/604377
2458 print('WARNING: Gerrit doesn\'t yet support cc-ing arbitrary emails.\n'
2459 ' Ignoring cc-ed emails. See http://crbug.com/604377.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002460
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002461 if change_desc.get_reviewers():
2462 refspec_opts.extend('r=' + email.strip()
2463 for email in change_desc.get_reviewers())
2464
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002465
2466 refspec_suffix = ''
2467 if refspec_opts:
2468 refspec_suffix = '%' + ','.join(refspec_opts)
2469 assert ' ' not in refspec_suffix, (
2470 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002471 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002472
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002473 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002474 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002475 print_stdout=True,
2476 # Flush after every line: useful for seeing progress when running as
2477 # recipe.
2478 filter_fn=lambda _: sys.stdout.flush())
2479
2480 if options.squash:
2481 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2482 change_numbers = [m.group(1)
2483 for m in map(regex.match, push_stdout.splitlines())
2484 if m]
2485 if len(change_numbers) != 1:
2486 DieWithError(
2487 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2488 'Change-Id: %s') % (len(change_numbers), change_id))
2489 self.SetIssue(change_numbers[0])
2490 RunGit(['config', 'branch.%s.gerritsquashhash' % self.GetBranch(),
2491 ref_to_push])
2492 return 0
2493
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002494 def _AddChangeIdToCommitMessage(self, options, args):
2495 """Re-commits using the current message, assumes the commit hook is in
2496 place.
2497 """
2498 log_desc = options.message or CreateDescriptionFromLog(args)
2499 git_command = ['commit', '--amend', '-m', log_desc]
2500 RunGit(git_command)
2501 new_log_desc = CreateDescriptionFromLog(args)
2502 if git_footers.get_footer_change_id(new_log_desc):
2503 print 'git-cl: Added Change-Id to commit message.'
2504 return new_log_desc
2505 else:
2506 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002507
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002508 def SetCQState(self, new_state):
2509 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
2510 # TODO(tandrii): maybe allow configurability in codereview.settings or by
2511 # self-discovery of label config for this CL using REST API.
2512 vote_map = {
2513 _CQState.NONE: 0,
2514 _CQState.DRY_RUN: 1,
2515 _CQState.COMMIT : 2,
2516 }
2517 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2518 labels={'Commit-Queue': vote_map[new_state]})
2519
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002520
2521_CODEREVIEW_IMPLEMENTATIONS = {
2522 'rietveld': _RietveldChangelistImpl,
2523 'gerrit': _GerritChangelistImpl,
2524}
2525
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002526
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002527def _add_codereview_select_options(parser):
2528 """Appends --gerrit and --rietveld options to force specific codereview."""
2529 parser.codereview_group = optparse.OptionGroup(
2530 parser, 'EXPERIMENTAL! Codereview override options')
2531 parser.add_option_group(parser.codereview_group)
2532 parser.codereview_group.add_option(
2533 '--gerrit', action='store_true',
2534 help='Force the use of Gerrit for codereview')
2535 parser.codereview_group.add_option(
2536 '--rietveld', action='store_true',
2537 help='Force the use of Rietveld for codereview')
2538
2539
2540def _process_codereview_select_options(parser, options):
2541 if options.gerrit and options.rietveld:
2542 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2543 options.forced_codereview = None
2544 if options.gerrit:
2545 options.forced_codereview = 'gerrit'
2546 elif options.rietveld:
2547 options.forced_codereview = 'rietveld'
2548
2549
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002550class ChangeDescription(object):
2551 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002552 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002553 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002554
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002555 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002556 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002557
agable@chromium.org42c20792013-09-12 17:34:49 +00002558 @property # www.logilab.org/ticket/89786
2559 def description(self): # pylint: disable=E0202
2560 return '\n'.join(self._description_lines)
2561
2562 def set_description(self, desc):
2563 if isinstance(desc, basestring):
2564 lines = desc.splitlines()
2565 else:
2566 lines = [line.rstrip() for line in desc]
2567 while lines and not lines[0]:
2568 lines.pop(0)
2569 while lines and not lines[-1]:
2570 lines.pop(-1)
2571 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002572
piman@chromium.org336f9122014-09-04 02:16:55 +00002573 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002574 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002575 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002576 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002577 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002578 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002579
agable@chromium.org42c20792013-09-12 17:34:49 +00002580 # Get the set of R= and TBR= lines and remove them from the desciption.
2581 regexp = re.compile(self.R_LINE)
2582 matches = [regexp.match(line) for line in self._description_lines]
2583 new_desc = [l for i, l in enumerate(self._description_lines)
2584 if not matches[i]]
2585 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002586
agable@chromium.org42c20792013-09-12 17:34:49 +00002587 # Construct new unified R= and TBR= lines.
2588 r_names = []
2589 tbr_names = []
2590 for match in matches:
2591 if not match:
2592 continue
2593 people = cleanup_list([match.group(2).strip()])
2594 if match.group(1) == 'TBR':
2595 tbr_names.extend(people)
2596 else:
2597 r_names.extend(people)
2598 for name in r_names:
2599 if name not in reviewers:
2600 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002601 if add_owners_tbr:
2602 owners_db = owners.Database(change.RepositoryRoot(),
2603 fopen=file, os_path=os.path, glob=glob.glob)
2604 all_reviewers = set(tbr_names + reviewers)
2605 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2606 all_reviewers)
2607 tbr_names.extend(owners_db.reviewers_for(missing_files,
2608 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002609 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2610 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2611
2612 # Put the new lines in the description where the old first R= line was.
2613 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2614 if 0 <= line_loc < len(self._description_lines):
2615 if new_tbr_line:
2616 self._description_lines.insert(line_loc, new_tbr_line)
2617 if new_r_line:
2618 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002619 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002620 if new_r_line:
2621 self.append_footer(new_r_line)
2622 if new_tbr_line:
2623 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002624
2625 def prompt(self):
2626 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002627 self.set_description([
2628 '# Enter a description of the change.',
2629 '# This will be displayed on the codereview site.',
2630 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002631 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002632 '--------------------',
2633 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002634
agable@chromium.org42c20792013-09-12 17:34:49 +00002635 regexp = re.compile(self.BUG_LINE)
2636 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00002637 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00002638 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002639 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002640 if not content:
2641 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002642 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002643
2644 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002645 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2646 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002647 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002648 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002649
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002650 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00002651 if self._description_lines:
2652 # Add an empty line if either the last line or the new line isn't a tag.
2653 last_line = self._description_lines[-1]
2654 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
2655 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2656 self._description_lines.append('')
2657 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002658
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002659 def get_reviewers(self):
2660 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002661 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
2662 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002663 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002664
2665
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002666def get_approving_reviewers(props):
2667 """Retrieves the reviewers that approved a CL from the issue properties with
2668 messages.
2669
2670 Note that the list may contain reviewers that are not committer, thus are not
2671 considered by the CQ.
2672 """
2673 return sorted(
2674 set(
2675 message['sender']
2676 for message in props['messages']
2677 if message['approval'] and message['sender'] in props['reviewers']
2678 )
2679 )
2680
2681
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002682def FindCodereviewSettingsFile(filename='codereview.settings'):
2683 """Finds the given file starting in the cwd and going up.
2684
2685 Only looks up to the top of the repository unless an
2686 'inherit-review-settings-ok' file exists in the root of the repository.
2687 """
2688 inherit_ok_file = 'inherit-review-settings-ok'
2689 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002690 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002691 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2692 root = '/'
2693 while True:
2694 if filename in os.listdir(cwd):
2695 if os.path.isfile(os.path.join(cwd, filename)):
2696 return open(os.path.join(cwd, filename))
2697 if cwd == root:
2698 break
2699 cwd = os.path.dirname(cwd)
2700
2701
2702def LoadCodereviewSettingsFromFile(fileobj):
2703 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002704 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002705
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002706 def SetProperty(name, setting, unset_error_ok=False):
2707 fullname = 'rietveld.' + name
2708 if setting in keyvals:
2709 RunGit(['config', fullname, keyvals[setting]])
2710 else:
2711 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2712
2713 SetProperty('server', 'CODE_REVIEW_SERVER')
2714 # Only server setting is required. Other settings can be absent.
2715 # In that case, we ignore errors raised during option deletion attempt.
2716 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002717 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002718 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2719 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002720 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002721 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002722 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2723 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002724 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002725 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002726 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002727 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2728 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002729
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002730 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002731 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002732
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002733 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
2734 RunGit(['config', 'gerrit.squash-uploads',
2735 keyvals['GERRIT_SQUASH_UPLOADS']])
2736
tandrii@chromium.org28253532016-04-14 13:46:56 +00002737 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00002738 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00002739 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
2740
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002741 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2742 #should be of the form
2743 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2744 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2745 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2746 keyvals['ORIGIN_URL_CONFIG']])
2747
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002748
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002749def urlretrieve(source, destination):
2750 """urllib is broken for SSL connections via a proxy therefore we
2751 can't use urllib.urlretrieve()."""
2752 with open(destination, 'w') as f:
2753 f.write(urllib2.urlopen(source).read())
2754
2755
ukai@chromium.org712d6102013-11-27 00:52:58 +00002756def hasSheBang(fname):
2757 """Checks fname is a #! script."""
2758 with open(fname) as f:
2759 return f.read(2).startswith('#!')
2760
2761
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00002762# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
2763def DownloadHooks(*args, **kwargs):
2764 pass
2765
2766
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002767def DownloadGerritHook(force):
2768 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002769
2770 Args:
2771 force: True to update hooks. False to install hooks if not present.
2772 """
2773 if not settings.GetIsGerrit():
2774 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00002775 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002776 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2777 if not os.access(dst, os.X_OK):
2778 if os.path.exists(dst):
2779 if not force:
2780 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002781 try:
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002782 print(
2783 'WARNING: installing Gerrit commit-msg hook.\n'
2784 ' This behavior of git cl will soon be disabled.\n'
2785 ' See bug http://crbug.com/579176.')
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002786 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002787 if not hasSheBang(dst):
2788 DieWithError('Not a script: %s\n'
2789 'You need to download from\n%s\n'
2790 'into .git/hooks/commit-msg and '
2791 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002792 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2793 except Exception:
2794 if os.path.exists(dst):
2795 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002796 DieWithError('\nFailed to download hooks.\n'
2797 'You need to download from\n%s\n'
2798 'into .git/hooks/commit-msg and '
2799 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002800
2801
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002802
2803def GetRietveldCodereviewSettingsInteractively():
2804 """Prompt the user for settings."""
2805 server = settings.GetDefaultServerUrl(error_ok=True)
2806 prompt = 'Rietveld server (host[:port])'
2807 prompt += ' [%s]' % (server or DEFAULT_SERVER)
2808 newserver = ask_for_data(prompt + ':')
2809 if not server and not newserver:
2810 newserver = DEFAULT_SERVER
2811 if newserver:
2812 newserver = gclient_utils.UpgradeToHttps(newserver)
2813 if newserver != server:
2814 RunGit(['config', 'rietveld.server', newserver])
2815
2816 def SetProperty(initial, caption, name, is_url):
2817 prompt = caption
2818 if initial:
2819 prompt += ' ("x" to clear) [%s]' % initial
2820 new_val = ask_for_data(prompt + ':')
2821 if new_val == 'x':
2822 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
2823 elif new_val:
2824 if is_url:
2825 new_val = gclient_utils.UpgradeToHttps(new_val)
2826 if new_val != initial:
2827 RunGit(['config', 'rietveld.' + name, new_val])
2828
2829 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
2830 SetProperty(settings.GetDefaultPrivateFlag(),
2831 'Private flag (rietveld only)', 'private', False)
2832 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
2833 'tree-status-url', False)
2834 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
2835 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
2836 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
2837 'run-post-upload-hook', False)
2838
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002839@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002840def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002841 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002842
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002843 print('WARNING: git cl config works for Rietveld only.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002844 'For Gerrit, see http://crbug.com/603116.')
2845 # TODO(tandrii): add Gerrit support as part of http://crbug.com/603116.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00002846 parser.add_option('--activate-update', action='store_true',
2847 help='activate auto-updating [rietveld] section in '
2848 '.git/config')
2849 parser.add_option('--deactivate-update', action='store_true',
2850 help='deactivate auto-updating [rietveld] section in '
2851 '.git/config')
2852 options, args = parser.parse_args(args)
2853
2854 if options.deactivate_update:
2855 RunGit(['config', 'rietveld.autoupdate', 'false'])
2856 return
2857
2858 if options.activate_update:
2859 RunGit(['config', '--unset', 'rietveld.autoupdate'])
2860 return
2861
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002862 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002863 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002864 return 0
2865
2866 url = args[0]
2867 if not url.endswith('codereview.settings'):
2868 url = os.path.join(url, 'codereview.settings')
2869
2870 # Load code review settings and download hooks (if available).
2871 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
2872 return 0
2873
2874
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002875def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002876 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002877 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
2878 branch = ShortBranchName(branchref)
2879 _, args = parser.parse_args(args)
2880 if not args:
2881 print("Current base-url:")
2882 return RunGit(['config', 'branch.%s.base-url' % branch],
2883 error_ok=False).strip()
2884 else:
2885 print("Setting base-url to %s" % args[0])
2886 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
2887 error_ok=False).strip()
2888
2889
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002890def color_for_status(status):
2891 """Maps a Changelist status to color, for CMDstatus and other tools."""
2892 return {
2893 'unsent': Fore.RED,
2894 'waiting': Fore.BLUE,
2895 'reply': Fore.YELLOW,
2896 'lgtm': Fore.GREEN,
2897 'commit': Fore.MAGENTA,
2898 'closed': Fore.CYAN,
2899 'error': Fore.WHITE,
2900 }.get(status, Fore.WHITE)
2901
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002902def get_cl_statuses(
clemensh@chromium.org81156e62016-04-25 08:22:13 +00002903 changes, fine_grained, max_processes=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002904 """Returns a blocking iterable of (branch, issue, color) for given branches.
2905
2906 If fine_grained is true, this will fetch CL statuses from the server.
2907 Otherwise, simply indicate if there's a matching url for the given branches.
2908
2909 If max_processes is specified, it is used as the maximum number of processes
2910 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
2911 spawned.
2912 """
2913 # Silence upload.py otherwise it becomes unwieldly.
2914 upload.verbosity = 0
2915
2916 if fine_grained:
2917 # Process one branch synchronously to work through authentication, then
2918 # spawn processes to process all the other branches in parallel.
clemensh@chromium.org81156e62016-04-25 08:22:13 +00002919 if changes:
2920 fetch = lambda cl: (cl, cl.GetStatus())
2921 yield fetch(changes[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002922
clemensh@chromium.org81156e62016-04-25 08:22:13 +00002923 changes_to_fetch = changes[1:]
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002924 pool = ThreadPool(
clemensh@chromium.org81156e62016-04-25 08:22:13 +00002925 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002926 if max_processes is not None
clemensh@chromium.org81156e62016-04-25 08:22:13 +00002927 else len(changes_to_fetch))
2928 for x in pool.imap_unordered(fetch, changes_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002929 yield x
2930 else:
2931 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.org81156e62016-04-25 08:22:13 +00002932 for cl in changes:
2933 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002934
rmistry@google.com2dd99862015-06-22 12:22:18 +00002935
2936def upload_branch_deps(cl, args):
2937 """Uploads CLs of local branches that are dependents of the current branch.
2938
2939 If the local branch dependency tree looks like:
2940 test1 -> test2.1 -> test3.1
2941 -> test3.2
2942 -> test2.2 -> test3.3
2943
2944 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
2945 run on the dependent branches in this order:
2946 test2.1, test3.1, test3.2, test2.2, test3.3
2947
2948 Note: This function does not rebase your local dependent branches. Use it when
2949 you make a change to the parent branch that will not conflict with its
2950 dependent branches, and you would like their dependencies updated in
2951 Rietveld.
2952 """
2953 if git_common.is_dirty_git_tree('upload-branch-deps'):
2954 return 1
2955
2956 root_branch = cl.GetBranch()
2957 if root_branch is None:
2958 DieWithError('Can\'t find dependent branches from detached HEAD state. '
2959 'Get on a branch!')
2960 if not cl.GetIssue() or not cl.GetPatchset():
2961 DieWithError('Current branch does not have an uploaded CL. We cannot set '
2962 'patchset dependencies without an uploaded CL.')
2963
2964 branches = RunGit(['for-each-ref',
2965 '--format=%(refname:short) %(upstream:short)',
2966 'refs/heads'])
2967 if not branches:
2968 print('No local branches found.')
2969 return 0
2970
2971 # Create a dictionary of all local branches to the branches that are dependent
2972 # on it.
2973 tracked_to_dependents = collections.defaultdict(list)
2974 for b in branches.splitlines():
2975 tokens = b.split()
2976 if len(tokens) == 2:
2977 branch_name, tracked = tokens
2978 tracked_to_dependents[tracked].append(branch_name)
2979
2980 print
2981 print 'The dependent local branches of %s are:' % root_branch
2982 dependents = []
2983 def traverse_dependents_preorder(branch, padding=''):
2984 dependents_to_process = tracked_to_dependents.get(branch, [])
2985 padding += ' '
2986 for dependent in dependents_to_process:
2987 print '%s%s' % (padding, dependent)
2988 dependents.append(dependent)
2989 traverse_dependents_preorder(dependent, padding)
2990 traverse_dependents_preorder(root_branch)
2991 print
2992
2993 if not dependents:
2994 print 'There are no dependent local branches for %s' % root_branch
2995 return 0
2996
2997 print ('This command will checkout all dependent branches and run '
2998 '"git cl upload".')
2999 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3000
andybons@chromium.org962f9462016-02-03 20:00:42 +00003001 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003002 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003003 args.extend(['-t', 'Updated patchset dependency'])
3004
rmistry@google.com2dd99862015-06-22 12:22:18 +00003005 # Record all dependents that failed to upload.
3006 failures = {}
3007 # Go through all dependents, checkout the branch and upload.
3008 try:
3009 for dependent_branch in dependents:
3010 print
3011 print '--------------------------------------'
3012 print 'Running "git cl upload" from %s:' % dependent_branch
3013 RunGit(['checkout', '-q', dependent_branch])
3014 print
3015 try:
3016 if CMDupload(OptionParser(), args) != 0:
3017 print 'Upload failed for %s!' % dependent_branch
3018 failures[dependent_branch] = 1
3019 except: # pylint: disable=W0702
3020 failures[dependent_branch] = 1
3021 print
3022 finally:
3023 # Swap back to the original root branch.
3024 RunGit(['checkout', '-q', root_branch])
3025
3026 print
3027 print 'Upload complete for dependent branches!'
3028 for dependent_branch in dependents:
3029 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
3030 print ' %s : %s' % (dependent_branch, upload_status)
3031 print
3032
3033 return 0
3034
3035
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003036def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003037 """Show status of changelists.
3038
3039 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003040 - Red not sent for review or broken
3041 - Blue waiting for review
3042 - Yellow waiting for you to reply to review
3043 - Green LGTM'ed
3044 - Magenta in the commit queue
3045 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003046
3047 Also see 'git cl comments'.
3048 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003049 parser.add_option('--field',
3050 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003051 parser.add_option('-f', '--fast', action='store_true',
3052 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003053 parser.add_option(
3054 '-j', '--maxjobs', action='store', type=int,
3055 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003056
3057 auth.add_auth_options(parser)
3058 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003059 if args:
3060 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003061 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003062
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003063 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003064 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003065 if options.field.startswith('desc'):
3066 print cl.GetDescription()
3067 elif options.field == 'id':
3068 issueid = cl.GetIssue()
3069 if issueid:
3070 print issueid
3071 elif options.field == 'patch':
3072 patchset = cl.GetPatchset()
3073 if patchset:
3074 print patchset
3075 elif options.field == 'url':
3076 url = cl.GetIssueURL()
3077 if url:
3078 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003079 return 0
3080
3081 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3082 if not branches:
3083 print('No local branch found.')
3084 return 0
3085
clemensh@chromium.org81156e62016-04-25 08:22:13 +00003086 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003087 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.org81156e62016-04-25 08:22:13 +00003088 for b in branches.splitlines()]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003089 print 'Branches associated with reviews:'
clemensh@chromium.org81156e62016-04-25 08:22:13 +00003090 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003091 fine_grained=not options.fast,
clemensh@chromium.org81156e62016-04-25 08:22:13 +00003092 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003093
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003094 branch_statuses = {}
clemensh@chromium.org81156e62016-04-25 08:22:13 +00003095 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3096 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3097 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003098 while branch not in branch_statuses:
clemensh@chromium.org81156e62016-04-25 08:22:13 +00003099 c, status = output.next()
3100 branch_statuses[c.GetBranch()] = status
3101 status = branch_statuses.pop(branch)
3102 url = cl.GetIssueURL()
3103 if url and (not status or status == 'error'):
3104 # The issue probably doesn't exist anymore.
3105 url += ' (broken)'
3106
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003107 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003108 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003109 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003110 color = ''
3111 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003112 status_str = '(%s)' % status if status else ''
3113 print ' %*s : %s%s %s%s' % (
clemensh@chromium.org81156e62016-04-25 08:22:13 +00003114 alignment, ShortBranchName(branch), color, url,
3115 status_str, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003116
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003117 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003118 print
3119 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003120 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003121 if not cl.GetIssue():
3122 print 'No issue assigned.'
3123 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003124 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00003125 if not options.fast:
3126 print 'Issue description:'
3127 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003128 return 0
3129
3130
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003131def colorize_CMDstatus_doc():
3132 """To be called once in main() to add colors to git cl status help."""
3133 colors = [i for i in dir(Fore) if i[0].isupper()]
3134
3135 def colorize_line(line):
3136 for color in colors:
3137 if color in line.upper():
3138 # Extract whitespaces first and the leading '-'.
3139 indent = len(line) - len(line.lstrip(' ')) + 1
3140 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3141 return line
3142
3143 lines = CMDstatus.__doc__.splitlines()
3144 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3145
3146
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003147@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003148def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003149 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003150
3151 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003152 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003153 parser.add_option('-r', '--reverse', action='store_true',
3154 help='Lookup the branch(es) for the specified issues. If '
3155 'no issues are specified, all branches with mapped '
3156 'issues will be listed.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003157 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003158 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003159 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003160
dnj@chromium.org406c4402015-03-03 17:22:28 +00003161 if options.reverse:
3162 branches = RunGit(['for-each-ref', 'refs/heads',
3163 '--format=%(refname:short)']).splitlines()
3164
3165 # Reverse issue lookup.
3166 issue_branch_map = {}
3167 for branch in branches:
3168 cl = Changelist(branchref=branch)
3169 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3170 if not args:
3171 args = sorted(issue_branch_map.iterkeys())
3172 for issue in args:
3173 if not issue:
3174 continue
3175 print 'Branch for issue number %s: %s' % (
3176 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
3177 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003178 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003179 if len(args) > 0:
3180 try:
3181 issue = int(args[0])
3182 except ValueError:
3183 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003184 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003185 cl.SetIssue(issue)
3186 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003187 return 0
3188
3189
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003190def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003191 """Shows or posts review comments for any changelist."""
3192 parser.add_option('-a', '--add-comment', dest='comment',
3193 help='comment to add to an issue')
3194 parser.add_option('-i', dest='issue',
3195 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003196 parser.add_option('-j', '--json-file',
3197 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003198 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003199 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003200 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003201
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003202 issue = None
3203 if options.issue:
3204 try:
3205 issue = int(options.issue)
3206 except ValueError:
3207 DieWithError('A review issue id is expected to be a number')
3208
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003209 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003210
3211 if options.comment:
3212 cl.AddComment(options.comment)
3213 return 0
3214
3215 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003216 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003217 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003218 summary.append({
3219 'date': message['date'],
3220 'lgtm': False,
3221 'message': message['text'],
3222 'not_lgtm': False,
3223 'sender': message['sender'],
3224 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003225 if message['disapproval']:
3226 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003227 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003228 elif message['approval']:
3229 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003230 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003231 elif message['sender'] == data['owner_email']:
3232 color = Fore.MAGENTA
3233 else:
3234 color = Fore.BLUE
3235 print '\n%s%s %s%s' % (
3236 color, message['date'].split('.', 1)[0], message['sender'],
3237 Fore.RESET)
3238 if message['text'].strip():
3239 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00003240 if options.json_file:
3241 with open(options.json_file, 'wb') as f:
3242 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003243 return 0
3244
3245
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003246def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003247 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003248 parser.add_option('-d', '--display', action='store_true',
3249 help='Display the description instead of opening an editor')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003250 auth.add_auth_options(parser)
3251 options, _ = parser.parse_args(args)
3252 auth_config = auth.extract_auth_config_from_options(options)
3253 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003254 if not cl.GetIssue():
3255 DieWithError('This branch has no associated changelist.')
3256 description = ChangeDescription(cl.GetDescription())
smut@google.com34fb6b12015-07-13 20:03:26 +00003257 if options.display:
3258 print description.description
3259 return 0
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003260 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003261 if cl.GetDescription() != description.description:
3262 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003263 return 0
3264
3265
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003266def CreateDescriptionFromLog(args):
3267 """Pulls out the commit log to use as a base for the CL description."""
3268 log_args = []
3269 if len(args) == 1 and not args[0].endswith('.'):
3270 log_args = [args[0] + '..']
3271 elif len(args) == 1 and args[0].endswith('...'):
3272 log_args = [args[0][:-1]]
3273 elif len(args) == 2:
3274 log_args = [args[0] + '..' + args[1]]
3275 else:
3276 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003277 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003278
3279
thestig@chromium.org44202a22014-03-11 19:22:18 +00003280def CMDlint(parser, args):
3281 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003282 parser.add_option('--filter', action='append', metavar='-x,+y',
3283 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003284 auth.add_auth_options(parser)
3285 options, args = parser.parse_args(args)
3286 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003287
3288 # Access to a protected member _XX of a client class
3289 # pylint: disable=W0212
3290 try:
3291 import cpplint
3292 import cpplint_chromium
3293 except ImportError:
3294 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
3295 return 1
3296
3297 # Change the current working directory before calling lint so that it
3298 # shows the correct base.
3299 previous_cwd = os.getcwd()
3300 os.chdir(settings.GetRoot())
3301 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003302 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003303 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3304 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003305 if not files:
3306 print "Cannot lint an empty CL"
3307 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003308
3309 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003310 command = args + files
3311 if options.filter:
3312 command = ['--filter=' + ','.join(options.filter)] + command
3313 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003314
3315 white_regex = re.compile(settings.GetLintRegex())
3316 black_regex = re.compile(settings.GetLintIgnoreRegex())
3317 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3318 for filename in filenames:
3319 if white_regex.match(filename):
3320 if black_regex.match(filename):
3321 print "Ignoring file %s" % filename
3322 else:
3323 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3324 extra_check_functions)
3325 else:
3326 print "Skipping file %s" % filename
3327 finally:
3328 os.chdir(previous_cwd)
3329 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
3330 if cpplint._cpplint_state.error_count != 0:
3331 return 1
3332 return 0
3333
3334
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003335def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003336 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003337 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003338 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003339 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003340 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003341 auth.add_auth_options(parser)
3342 options, args = parser.parse_args(args)
3343 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003344
sbc@chromium.org71437c02015-04-09 19:29:40 +00003345 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00003346 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003347 return 1
3348
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003349 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003350 if args:
3351 base_branch = args[0]
3352 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003353 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003354 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003355
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003356 cl.RunHook(
3357 committing=not options.upload,
3358 may_prompt=False,
3359 verbose=options.verbose,
3360 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003361 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003362
3363
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003364def GenerateGerritChangeId(message):
3365 """Returns Ixxxxxx...xxx change id.
3366
3367 Works the same way as
3368 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3369 but can be called on demand on all platforms.
3370
3371 The basic idea is to generate git hash of a state of the tree, original commit
3372 message, author/committer info and timestamps.
3373 """
3374 lines = []
3375 tree_hash = RunGitSilent(['write-tree'])
3376 lines.append('tree %s' % tree_hash.strip())
3377 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3378 if code == 0:
3379 lines.append('parent %s' % parent.strip())
3380 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3381 lines.append('author %s' % author.strip())
3382 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3383 lines.append('committer %s' % committer.strip())
3384 lines.append('')
3385 # Note: Gerrit's commit-hook actually cleans message of some lines and
3386 # whitespace. This code is not doing this, but it clearly won't decrease
3387 # entropy.
3388 lines.append(message)
3389 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3390 stdin='\n'.join(lines))
3391 return 'I%s' % change_hash.strip()
3392
3393
wittman@chromium.org455dc922015-01-26 20:15:50 +00003394def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3395 """Computes the remote branch ref to use for the CL.
3396
3397 Args:
3398 remote (str): The git remote for the CL.
3399 remote_branch (str): The git remote branch for the CL.
3400 target_branch (str): The target branch specified by the user.
3401 pending_prefix (str): The pending prefix from the settings.
3402 """
3403 if not (remote and remote_branch):
3404 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003405
wittman@chromium.org455dc922015-01-26 20:15:50 +00003406 if target_branch:
3407 # Cannonicalize branch references to the equivalent local full symbolic
3408 # refs, which are then translated into the remote full symbolic refs
3409 # below.
3410 if '/' not in target_branch:
3411 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3412 else:
3413 prefix_replacements = (
3414 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3415 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3416 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3417 )
3418 match = None
3419 for regex, replacement in prefix_replacements:
3420 match = re.search(regex, target_branch)
3421 if match:
3422 remote_branch = target_branch.replace(match.group(0), replacement)
3423 break
3424 if not match:
3425 # This is a branch path but not one we recognize; use as-is.
3426 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003427 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3428 # Handle the refs that need to land in different refs.
3429 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003430
wittman@chromium.org455dc922015-01-26 20:15:50 +00003431 # Create the true path to the remote branch.
3432 # Does the following translation:
3433 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3434 # * refs/remotes/origin/master -> refs/heads/master
3435 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3436 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3437 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3438 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3439 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3440 'refs/heads/')
3441 elif remote_branch.startswith('refs/remotes/branch-heads'):
3442 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3443 # If a pending prefix exists then replace refs/ with it.
3444 if pending_prefix:
3445 remote_branch = remote_branch.replace('refs/', pending_prefix)
3446 return remote_branch
3447
3448
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003449def cleanup_list(l):
3450 """Fixes a list so that comma separated items are put as individual items.
3451
3452 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3453 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3454 """
3455 items = sum((i.split(',') for i in l), [])
3456 stripped_items = (i.strip() for i in items)
3457 return sorted(filter(None, stripped_items))
3458
3459
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003460@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003461def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003462 """Uploads the current changelist to codereview.
3463
3464 Can skip dependency patchset uploads for a branch by running:
3465 git config branch.branch_name.skip-deps-uploads True
3466 To unset run:
3467 git config --unset branch.branch_name.skip-deps-uploads
3468 Can also set the above globally by using the --global flag.
3469 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003470 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3471 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003472 parser.add_option('--bypass-watchlists', action='store_true',
3473 dest='bypass_watchlists',
3474 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003475 parser.add_option('-f', action='store_true', dest='force',
3476 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003477 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003478 parser.add_option('-t', dest='title',
3479 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003480 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003481 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003482 help='reviewer email addresses')
3483 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003484 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003485 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003486 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003487 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003488 parser.add_option('--emulate_svn_auto_props',
3489 '--emulate-svn-auto-props',
3490 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003491 dest="emulate_svn_auto_props",
3492 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003493 parser.add_option('-c', '--use-commit-queue', action='store_true',
3494 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003495 parser.add_option('--private', action='store_true',
3496 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003497 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003498 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003499 metavar='TARGET',
3500 help='Apply CL to remote ref TARGET. ' +
3501 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003502 parser.add_option('--squash', action='store_true',
3503 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003504 parser.add_option('--no-squash', action='store_true',
3505 help='Don\'t squash multiple commits into one ' +
3506 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003507 parser.add_option('--email', default=None,
3508 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003509 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3510 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003511 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3512 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003513 help='Send the patchset to do a CQ dry run right after '
3514 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003515 parser.add_option('--dependencies', action='store_true',
3516 help='Uploads CLs of all the local branches that depend on '
3517 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003518
rmistry@google.com2dd99862015-06-22 12:22:18 +00003519 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003520 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003521 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003522 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003523 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003524 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003525 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003526
sbc@chromium.org71437c02015-04-09 19:29:40 +00003527 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003528 return 1
3529
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003530 options.reviewers = cleanup_list(options.reviewers)
3531 options.cc = cleanup_list(options.cc)
3532
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003533 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3534 settings.GetIsGerrit()
3535
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003536 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00003537 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003538
3539
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003540def IsSubmoduleMergeCommit(ref):
3541 # When submodules are added to the repo, we expect there to be a single
3542 # non-git-svn merge commit at remote HEAD with a signature comment.
3543 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003544 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003545 return RunGit(cmd) != ''
3546
3547
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003548def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003549 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003550
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003551 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3552 upstream and closes the issue automatically and atomically.
3553
3554 Otherwise (in case of Rietveld):
3555 Squashes branch into a single commit.
3556 Updates changelog with metadata (e.g. pointer to review).
3557 Pushes/dcommits the code upstream.
3558 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003559 """
3560 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3561 help='bypass upload presubmit hook')
3562 parser.add_option('-m', dest='message',
3563 help="override review description")
3564 parser.add_option('-f', action='store_true', dest='force',
3565 help="force yes to questions (don't prompt)")
3566 parser.add_option('-c', dest='contributor',
3567 help="external contributor for patch (appended to " +
3568 "description and used as author for git). Should be " +
3569 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003570 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003571 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003572 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003573 auth_config = auth.extract_auth_config_from_options(options)
3574
3575 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003576
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003577 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3578 if cl.IsGerrit():
3579 if options.message:
3580 # This could be implemented, but it requires sending a new patch to
3581 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3582 # Besides, Gerrit has the ability to change the commit message on submit
3583 # automatically, thus there is no need to support this option (so far?).
3584 parser.error('-m MESSAGE option is not supported for Gerrit.')
3585 if options.contributor:
3586 parser.error(
3587 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3588 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3589 'the contributor\'s "name <email>". If you can\'t upload such a '
3590 'commit for review, contact your repository admin and request'
3591 '"Forge-Author" permission.')
3592 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3593 options.verbose)
3594
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003595 current = cl.GetBranch()
3596 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3597 if not settings.GetIsGitSvn() and remote == '.':
3598 print
3599 print 'Attempting to push branch %r into another local branch!' % current
3600 print
3601 print 'Either reparent this branch on top of origin/master:'
3602 print ' git reparent-branch --root'
3603 print
3604 print 'OR run `git rebase-update` if you think the parent branch is already'
3605 print 'committed.'
3606 print
3607 print ' Current parent: %r' % upstream_branch
3608 return 1
3609
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003610 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003611 # Default to merging against our best guess of the upstream branch.
3612 args = [cl.GetUpstreamBranch()]
3613
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003614 if options.contributor:
3615 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
3616 print "Please provide contibutor as 'First Last <email@example.com>'"
3617 return 1
3618
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003619 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003620 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003621
sbc@chromium.org71437c02015-04-09 19:29:40 +00003622 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003623 return 1
3624
3625 # This rev-list syntax means "show all commits not in my branch that
3626 # are in base_branch".
3627 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3628 base_branch]).splitlines()
3629 if upstream_commits:
3630 print ('Base branch "%s" has %d commits '
3631 'not in this branch.' % (base_branch, len(upstream_commits)))
3632 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
3633 return 1
3634
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003635 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003636 svn_head = None
3637 if cmd == 'dcommit' or base_has_submodules:
3638 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
3639 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003640
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003641 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003642 # If the base_head is a submodule merge commit, the first parent of the
3643 # base_head should be a git-svn commit, which is what we're interested in.
3644 base_svn_head = base_branch
3645 if base_has_submodules:
3646 base_svn_head += '^1'
3647
3648 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003649 if extra_commits:
3650 print ('This branch has %d additional commits not upstreamed yet.'
3651 % len(extra_commits.splitlines()))
3652 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
3653 'before attempting to %s.' % (base_branch, cmd))
3654 return 1
3655
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003656 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003657 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003658 author = None
3659 if options.contributor:
3660 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003661 hook_results = cl.RunHook(
3662 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003663 may_prompt=not options.force,
3664 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003665 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003666 if not hook_results.should_continue():
3667 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003668
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003669 # Check the tree status if the tree status URL is set.
3670 status = GetTreeStatus()
3671 if 'closed' == status:
3672 print('The tree is closed. Please wait for it to reopen. Use '
3673 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3674 return 1
3675 elif 'unknown' == status:
3676 print('Unable to determine tree status. Please verify manually and '
3677 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3678 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003679
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003680 change_desc = ChangeDescription(options.message)
3681 if not change_desc.description and cl.GetIssue():
3682 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003683
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003684 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00003685 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003686 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00003687 else:
3688 print 'No description set.'
3689 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
3690 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003691
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003692 # Keep a separate copy for the commit message, because the commit message
3693 # contains the link to the Rietveld issue, while the Rietveld message contains
3694 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003695 # Keep a separate copy for the commit message.
3696 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00003697 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003698
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003699 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00003700 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00003701 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00003702 # after it. Add a period on a new line to circumvent this. Also add a space
3703 # before the period to make sure that Gitiles continues to correctly resolve
3704 # the URL.
3705 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003706 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003707 commit_desc.append_footer('Patch from %s.' % options.contributor)
3708
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00003709 print('Description:')
3710 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003711
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003712 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003713 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00003714 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003715
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003716 # We want to squash all this branch's commits into one commit with the proper
3717 # description. We do this by doing a "reset --soft" to the base branch (which
3718 # keeps the working copy the same), then dcommitting that. If origin/master
3719 # has a submodule merge commit, we'll also need to cherry-pick the squashed
3720 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003721 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003722 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
3723 # Delete the branches if they exist.
3724 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
3725 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
3726 result = RunGitWithCode(showref_cmd)
3727 if result[0] == 0:
3728 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003729
3730 # We might be in a directory that's present in this branch but not in the
3731 # trunk. Move up to the top of the tree so that git commands that expect a
3732 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003733 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003734 if rel_base_path:
3735 os.chdir(rel_base_path)
3736
3737 # Stuff our change into the merge branch.
3738 # We wrap in a try...finally block so if anything goes wrong,
3739 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003740 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003741 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003742 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003743 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003744 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00003745 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003746 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003747 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003748 RunGit(
3749 [
3750 'commit', '--author', options.contributor,
3751 '-m', commit_desc.description,
3752 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003753 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003754 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003755 if base_has_submodules:
3756 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
3757 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
3758 RunGit(['checkout', CHERRY_PICK_BRANCH])
3759 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003760 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003761 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003762 mirror = settings.GetGitMirror(remote)
3763 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003764 pending_prefix = settings.GetPendingRefPrefix()
3765 if not pending_prefix or branch.startswith(pending_prefix):
3766 # If not using refs/pending/heads/* at all, or target ref is already set
3767 # to pending, then push to the target ref directly.
3768 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003769 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003770 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003771 else:
3772 # Cherry-pick the change on top of pending ref and then push it.
3773 assert branch.startswith('refs/'), branch
3774 assert pending_prefix[-1] == '/', pending_prefix
3775 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003776 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003777 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003778 if retcode == 0:
3779 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003780 else:
3781 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003782 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003783 'svn', 'dcommit',
3784 '-C%s' % options.similarity,
3785 '--no-rebase', '--rmdir',
3786 ]
3787 if settings.GetForceHttpsCommitUrl():
3788 # Allow forcing https commit URLs for some projects that don't allow
3789 # committing to http URLs (like Google Code).
3790 remote_url = cl.GetGitSvnRemoteUrl()
3791 if urlparse.urlparse(remote_url).scheme == 'http':
3792 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003793 cmd_args.append('--commit-url=%s' % remote_url)
3794 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003795 if 'Committed r' in output:
3796 revision = re.match(
3797 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
3798 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003799 finally:
3800 # And then swap back to the original branch and clean up.
3801 RunGit(['checkout', '-q', cl.GetBranch()])
3802 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003803 if base_has_submodules:
3804 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003805
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003806 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003807 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003808 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003809
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003810 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003811 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003812 try:
3813 revision = WaitForRealCommit(remote, revision, base_branch, branch)
3814 # We set pushed_to_pending to False, since it made it all the way to the
3815 # real ref.
3816 pushed_to_pending = False
3817 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003818 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003819
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003820 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003821 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003822 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003823 if not to_pending:
3824 if viewvc_url and revision:
3825 change_desc.append_footer(
3826 'Committed: %s%s' % (viewvc_url, revision))
3827 elif revision:
3828 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003829 print ('Closing issue '
3830 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003831 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003832 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003833 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00003834 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00003835 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00003836 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003837 if options.bypass_hooks:
3838 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
3839 else:
3840 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00003841 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003842 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003843
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003844 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003845 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
3846 print 'The commit is in the pending queue (%s).' % pending_ref
3847 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00003848 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003849 'footer.' % branch)
3850
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003851 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
3852 if os.path.isfile(hook):
3853 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003854
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003855 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003856
3857
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003858def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
3859 print
3860 print 'Waiting for commit to be landed on %s...' % real_ref
3861 print '(If you are impatient, you may Ctrl-C once without harm)'
3862 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
3863 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003864 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003865
3866 loop = 0
3867 while True:
3868 sys.stdout.write('fetching (%d)... \r' % loop)
3869 sys.stdout.flush()
3870 loop += 1
3871
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003872 if mirror:
3873 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003874 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
3875 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
3876 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
3877 for commit in commits.splitlines():
3878 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
3879 print 'Found commit on %s' % real_ref
3880 return commit
3881
3882 current_rev = to_rev
3883
3884
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003885def PushToGitPending(remote, pending_ref, upstream_ref):
3886 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
3887
3888 Returns:
3889 (retcode of last operation, output log of last operation).
3890 """
3891 assert pending_ref.startswith('refs/'), pending_ref
3892 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
3893 cherry = RunGit(['rev-parse', 'HEAD']).strip()
3894 code = 0
3895 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003896 max_attempts = 3
3897 attempts_left = max_attempts
3898 while attempts_left:
3899 if attempts_left != max_attempts:
3900 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
3901 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003902
3903 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003904 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003905 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003906 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003907 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003908 print 'Fetch failed with exit code %d.' % code
3909 if out.strip():
3910 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003911 continue
3912
3913 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003914 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003915 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003916 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003917 if code:
3918 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003919 'Your patch doesn\'t apply cleanly to ref \'%s\', '
3920 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003921 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
3922 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003923 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003924 return code, out
3925
3926 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003927 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003928 code, out = RunGitWithCode(
3929 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
3930 if code == 0:
3931 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003932 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003933 return code, out
3934
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003935 print 'Push failed with exit code %d.' % code
3936 if out.strip():
3937 print out.strip()
3938 if IsFatalPushFailure(out):
3939 print (
3940 'Fatal push error. Make sure your .netrc credentials and git '
3941 'user.email are correct and you have push access to the repo.')
3942 return code, out
3943
3944 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003945 return code, out
3946
3947
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003948def IsFatalPushFailure(push_stdout):
3949 """True if retrying push won't help."""
3950 return '(prohibited by Gerrit)' in push_stdout
3951
3952
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003953@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003954def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003955 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003956 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003957 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003958 # If it looks like previous commits were mirrored with git-svn.
3959 message = """This repository appears to be a git-svn mirror, but no
3960upstream SVN master is set. You probably need to run 'git auto-svn' once."""
3961 else:
3962 message = """This doesn't appear to be an SVN repository.
3963If your project has a true, writeable git repository, you probably want to run
3964'git cl land' instead.
3965If your project has a git mirror of an upstream SVN master, you probably need
3966to run 'git svn init'.
3967
3968Using the wrong command might cause your commit to appear to succeed, and the
3969review to be closed, without actually landing upstream. If you choose to
3970proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00003971 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00003972 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003973 return SendUpstream(parser, args, 'dcommit')
3974
3975
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003976@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003977def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003978 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003979 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003980 print('This appears to be an SVN repository.')
3981 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003982 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00003983 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003984 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003985
3986
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003987@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003988def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00003989 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003990 parser.add_option('-b', dest='newbranch',
3991 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003992 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003993 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003994 parser.add_option('-d', '--directory', action='store', metavar='DIR',
3995 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003996 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003997 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003998 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003999 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004000 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004001 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004002
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004003
4004 group = optparse.OptionGroup(
4005 parser,
4006 'Options for continuing work on the current issue uploaded from a '
4007 'different clone (e.g. different machine). Must be used independently '
4008 'from the other options. No issue number should be specified, and the '
4009 'branch must have an issue number associated with it')
4010 group.add_option('--reapply', action='store_true', dest='reapply',
4011 help='Reset the branch and reapply the issue.\n'
4012 'CAUTION: This will undo any local changes in this '
4013 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004014
4015 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004016 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004017 parser.add_option_group(group)
4018
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004019 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004020 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004021 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004022 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004023 auth_config = auth.extract_auth_config_from_options(options)
4024
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004025 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004026
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004027 issue_arg = None
4028 if options.reapply :
4029 if len(args) > 0:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004030 parser.error('--reapply implies no additional arguments.')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004031
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004032 issue_arg = cl.GetIssue()
4033 upstream = cl.GetUpstreamBranch()
4034 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004035 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004036
4037 RunGit(['reset', '--hard', upstream])
4038 if options.pull:
4039 RunGit(['pull'])
4040 else:
4041 if len(args) != 1:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004042 parser.error('Must specify issue number or url')
4043 issue_arg = args[0]
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004044
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004045 if not issue_arg:
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004046 parser.print_help()
4047 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004048
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004049 if cl.IsGerrit():
4050 if options.reject:
4051 parser.error('--reject is not supported with Gerrit codereview.')
4052 if options.nocommit:
4053 parser.error('--nocommit is not supported with Gerrit codereview.')
4054 if options.directory:
4055 parser.error('--directory is not supported with Gerrit codereview.')
4056
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004057 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004058 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004059 return 1
4060
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004061 if options.newbranch:
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004062 if options.reapply:
4063 parser.error("--reapply excludes any option other than --pull")
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004064 if options.force:
4065 RunGit(['branch', '-D', options.newbranch],
4066 stderr=subprocess2.PIPE, error_ok=True)
4067 RunGit(['checkout', '-b', options.newbranch,
4068 Changelist().GetUpstreamBranch()])
4069
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004070 return cl.CMDPatchIssue(issue_arg, options.reject, options.nocommit,
4071 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004072
4073
4074def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004075 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004076 # Provide a wrapper for git svn rebase to help avoid accidental
4077 # git svn dcommit.
4078 # It's the only command that doesn't use parser at all since we just defer
4079 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004080
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004081 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004082
4083
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004084def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004085 """Fetches the tree status and returns either 'open', 'closed',
4086 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004087 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004088 if url:
4089 status = urllib2.urlopen(url).read().lower()
4090 if status.find('closed') != -1 or status == '0':
4091 return 'closed'
4092 elif status.find('open') != -1 or status == '1':
4093 return 'open'
4094 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004095 return 'unset'
4096
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004097
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004098def GetTreeStatusReason():
4099 """Fetches the tree status from a json url and returns the message
4100 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004101 url = settings.GetTreeStatusUrl()
4102 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004103 connection = urllib2.urlopen(json_url)
4104 status = json.loads(connection.read())
4105 connection.close()
4106 return status['message']
4107
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004108
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004109def GetBuilderMaster(bot_list):
4110 """For a given builder, fetch the master from AE if available."""
4111 map_url = 'https://builders-map.appspot.com/'
4112 try:
4113 master_map = json.load(urllib2.urlopen(map_url))
4114 except urllib2.URLError as e:
4115 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4116 (map_url, e))
4117 except ValueError as e:
4118 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4119 if not master_map:
4120 return None, 'Failed to build master map.'
4121
4122 result_master = ''
4123 for bot in bot_list:
4124 builder = bot.split(':', 1)[0]
4125 master_list = master_map.get(builder, [])
4126 if not master_list:
4127 return None, ('No matching master for builder %s.' % builder)
4128 elif len(master_list) > 1:
4129 return None, ('The builder name %s exists in multiple masters %s.' %
4130 (builder, master_list))
4131 else:
4132 cur_master = master_list[0]
4133 if not result_master:
4134 result_master = cur_master
4135 elif result_master != cur_master:
4136 return None, 'The builders do not belong to the same master.'
4137 return result_master, None
4138
4139
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004140def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004141 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004142 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004143 status = GetTreeStatus()
4144 if 'unset' == status:
4145 print 'You must configure your tree status URL by running "git cl config".'
4146 return 2
4147
4148 print "The tree is %s" % status
4149 print
4150 print GetTreeStatusReason()
4151 if status != 'open':
4152 return 1
4153 return 0
4154
4155
maruel@chromium.org15192402012-09-06 12:38:29 +00004156def CMDtry(parser, args):
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004157 """Triggers try jobs through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00004158 group = optparse.OptionGroup(parser, "Try job options")
4159 group.add_option(
4160 "-b", "--bot", action="append",
4161 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
4162 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004163 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00004164 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004165 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00004166 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004167 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00004168 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004169 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004170 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004171 "-r", "--revision",
4172 help="Revision to use for the try job; default: the "
4173 "revision will be determined by the try server; see "
4174 "its waterfall for more info")
4175 group.add_option(
4176 "-c", "--clobber", action="store_true", default=False,
4177 help="Force a clobber before building; e.g. don't do an "
4178 "incremental build")
4179 group.add_option(
4180 "--project",
4181 help="Override which project to use. Projects are defined "
4182 "server-side to define what default bot set to use")
4183 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004184 "-p", "--property", dest="properties", action="append", default=[],
4185 help="Specify generic properties in the form -p key1=value1 -p "
4186 "key2=value2 etc (buildbucket only). The value will be treated as "
4187 "json if decodable, or as string otherwise.")
4188 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004189 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004190 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004191 "--use-rietveld", action="store_true", default=False,
4192 help="Use Rietveld to trigger try jobs.")
4193 group.add_option(
4194 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4195 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004196 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004197 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004198 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004199 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004200
machenbach@chromium.org45453142015-09-15 08:45:22 +00004201 if options.use_rietveld and options.properties:
4202 parser.error('Properties can only be specified with buildbucket')
4203
4204 # Make sure that all properties are prop=value pairs.
4205 bad_params = [x for x in options.properties if '=' not in x]
4206 if bad_params:
4207 parser.error('Got properties with missing "=": %s' % bad_params)
4208
maruel@chromium.org15192402012-09-06 12:38:29 +00004209 if args:
4210 parser.error('Unknown arguments: %s' % args)
4211
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004212 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004213 if not cl.GetIssue():
4214 parser.error('Need to upload first')
4215
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004216 if cl.IsGerrit():
4217 parser.error(
4218 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4219 'If your project has Commit Queue, dry run is a workaround:\n'
4220 ' git cl set-commit --dry-run')
4221 # Code below assumes Rietveld issue.
4222 # TODO(tandrii): actually implement for Gerrit http://crbug.com/599931.
4223
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004224 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004225 if props.get('closed'):
4226 parser.error('Cannot send tryjobs for a closed CL')
4227
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004228 if props.get('private'):
4229 parser.error('Cannot use trybots with private issue')
4230
maruel@chromium.org15192402012-09-06 12:38:29 +00004231 if not options.name:
4232 options.name = cl.GetBranch()
4233
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004234 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004235 options.master, err_msg = GetBuilderMaster(options.bot)
4236 if err_msg:
4237 parser.error('Tryserver master cannot be found because: %s\n'
4238 'Please manually specify the tryserver master'
4239 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004240
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004241 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004242 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004243 if not options.bot:
4244 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004245
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004246 # Get try masters from PRESUBMIT.py files.
4247 masters = presubmit_support.DoGetTryMasters(
4248 change,
4249 change.LocalPaths(),
4250 settings.GetRoot(),
4251 None,
4252 None,
4253 options.verbose,
4254 sys.stdout)
4255 if masters:
4256 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004257
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004258 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4259 options.bot = presubmit_support.DoGetTrySlaves(
4260 change,
4261 change.LocalPaths(),
4262 settings.GetRoot(),
4263 None,
4264 None,
4265 options.verbose,
4266 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004267
4268 if not options.bot:
4269 # Get try masters from cq.cfg if any.
4270 # TODO(tandrii): some (but very few) projects store cq.cfg in different
4271 # location.
4272 cq_cfg = os.path.join(change.RepositoryRoot(),
4273 'infra', 'config', 'cq.cfg')
4274 if os.path.exists(cq_cfg):
4275 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00004276 cq_masters = commit_queue.get_master_builder_map(
4277 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004278 for master, builders in cq_masters.iteritems():
4279 for builder in builders:
4280 # Skip presubmit builders, because these will fail without LGTM.
4281 if 'presubmit' not in builder.lower():
4282 masters.setdefault(master, {})[builder] = ['defaulttests']
4283 if masters:
4284 return masters
4285
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004286 if not options.bot:
4287 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00004288
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004289 builders_and_tests = {}
4290 # TODO(machenbach): The old style command-line options don't support
4291 # multiple try masters yet.
4292 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4293 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4294
4295 for bot in old_style:
4296 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004297 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004298 elif ',' in bot:
4299 parser.error('Specify one bot per --bot flag')
4300 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004301 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004302
4303 for bot, tests in new_style:
4304 builders_and_tests.setdefault(bot, []).extend(tests)
4305
4306 # Return a master map with one master to be backwards compatible. The
4307 # master name defaults to an empty string, which will cause the master
4308 # not to be set on rietveld (deprecated).
4309 return {options.master: builders_and_tests}
4310
4311 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004312
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004313 for builders in masters.itervalues():
4314 if any('triggered' in b for b in builders):
4315 print >> sys.stderr, (
4316 'ERROR You are trying to send a job to a triggered bot. This type of'
4317 ' bot requires an\ninitial job from a parent (usually a builder). '
4318 'Instead send your job to the parent.\n'
4319 'Bot list: %s' % builders)
4320 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004321
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004322 patchset = cl.GetMostRecentPatchset()
4323 if patchset and patchset != cl.GetPatchset():
4324 print(
4325 '\nWARNING Mismatch between local config and server. Did a previous '
4326 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4327 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004328 if options.luci:
4329 trigger_luci_job(cl, masters, options)
4330 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004331 try:
4332 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4333 except BuildbucketResponseException as ex:
4334 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004335 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004336 except Exception as e:
4337 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4338 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
4339 e, stacktrace)
4340 return 1
4341 else:
4342 try:
4343 cl.RpcServer().trigger_distributed_try_jobs(
4344 cl.GetIssue(), patchset, options.name, options.clobber,
4345 options.revision, masters)
4346 except urllib2.HTTPError as e:
4347 if e.code == 404:
4348 print('404 from rietveld; '
4349 'did you mean to use "git try" instead of "git cl try"?')
4350 return 1
4351 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004352
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004353 for (master, builders) in sorted(masters.iteritems()):
4354 if master:
4355 print 'Master: %s' % master
4356 length = max(len(builder) for builder in builders)
4357 for builder in sorted(builders):
4358 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00004359 return 0
4360
4361
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004362def CMDtry_results(parser, args):
4363 group = optparse.OptionGroup(parser, "Try job results options")
4364 group.add_option(
4365 "-p", "--patchset", type=int, help="patchset number if not current.")
4366 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004367 "--print-master", action='store_true', help="print master name as well.")
4368 group.add_option(
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004369 "--color", action='store_true', default=setup_color.IS_TTY,
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004370 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004371 group.add_option(
4372 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4373 help="Host of buildbucket. The default host is %default.")
4374 parser.add_option_group(group)
4375 auth.add_auth_options(parser)
4376 options, args = parser.parse_args(args)
4377 if args:
4378 parser.error('Unrecognized args: %s' % ' '.join(args))
4379
4380 auth_config = auth.extract_auth_config_from_options(options)
4381 cl = Changelist(auth_config=auth_config)
4382 if not cl.GetIssue():
4383 parser.error('Need to upload first')
4384
4385 if not options.patchset:
4386 options.patchset = cl.GetMostRecentPatchset()
4387 if options.patchset and options.patchset != cl.GetPatchset():
4388 print(
4389 '\nWARNING Mismatch between local config and server. Did a previous '
4390 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4391 'Continuing using\npatchset %s.\n' % options.patchset)
4392 try:
4393 jobs = fetch_try_jobs(auth_config, cl, options)
4394 except BuildbucketResponseException as ex:
4395 print 'Buildbucket error: %s' % ex
4396 return 1
4397 except Exception as e:
4398 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4399 print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
4400 e, stacktrace)
4401 return 1
4402 print_tryjobs(options, jobs)
4403 return 0
4404
4405
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004406@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004407def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004408 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004409 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004410 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004411 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004412
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004413 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004414 if args:
4415 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004416 branch = cl.GetBranch()
4417 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004418 cl = Changelist()
4419 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004420
4421 # Clear configured merge-base, if there is one.
4422 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004423 else:
4424 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004425 return 0
4426
4427
thestig@chromium.org00858c82013-12-02 23:08:03 +00004428def CMDweb(parser, args):
4429 """Opens the current CL in the web browser."""
4430 _, args = parser.parse_args(args)
4431 if args:
4432 parser.error('Unrecognized args: %s' % ' '.join(args))
4433
4434 issue_url = Changelist().GetIssueURL()
4435 if not issue_url:
4436 print >> sys.stderr, 'ERROR No issue to open'
4437 return 1
4438
4439 webbrowser.open(issue_url)
4440 return 0
4441
4442
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004443def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004444 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004445 parser.add_option('-d', '--dry-run', action='store_true',
4446 help='trigger in dry run mode')
4447 parser.add_option('-c', '--clear', action='store_true',
4448 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004449 auth.add_auth_options(parser)
4450 options, args = parser.parse_args(args)
4451 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004452 if args:
4453 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004454 if options.dry_run and options.clear:
4455 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4456
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004457 cl = Changelist(auth_config=auth_config)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004458 if options.clear:
4459 state = _CQState.CLEAR
4460 elif options.dry_run:
4461 state = _CQState.DRY_RUN
4462 else:
4463 state = _CQState.COMMIT
4464 if not cl.GetIssue():
4465 parser.error('Must upload the issue first')
4466 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004467 return 0
4468
4469
groby@chromium.org411034a2013-02-26 15:12:01 +00004470def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004471 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004472 auth.add_auth_options(parser)
4473 options, args = parser.parse_args(args)
4474 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004475 if args:
4476 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004477 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00004478 # Ensure there actually is an issue to close.
4479 cl.GetDescription()
4480 cl.CloseIssue()
4481 return 0
4482
4483
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004484def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004485 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004486 auth.add_auth_options(parser)
4487 options, args = parser.parse_args(args)
4488 auth_config = auth.extract_auth_config_from_options(options)
4489 if args:
4490 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004491
4492 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004493 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004494 # Staged changes would be committed along with the patch from last
4495 # upload, hence counted toward the "last upload" side in the final
4496 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004497 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004498 return 1
4499
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004500 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004501 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004502 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004503 if not issue:
4504 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004505 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004506 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004507
4508 # Create a new branch based on the merge-base
4509 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004510 # Clear cached branch in cl object, to avoid overwriting original CL branch
4511 # properties.
4512 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004513 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004514 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004515 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004516 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004517 return rtn
4518
wychen@chromium.org06928532015-02-03 02:11:29 +00004519 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004520 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00004521 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004522 finally:
4523 RunGit(['checkout', '-q', branch])
4524 RunGit(['branch', '-D', TMP_BRANCH])
4525
4526 return 0
4527
4528
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004529def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004530 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004531 parser.add_option(
4532 '--no-color',
4533 action='store_true',
4534 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004535 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004536 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004537 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004538
4539 author = RunGit(['config', 'user.email']).strip() or None
4540
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004541 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004542
4543 if args:
4544 if len(args) > 1:
4545 parser.error('Unknown args')
4546 base_branch = args[0]
4547 else:
4548 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004549 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004550
4551 change = cl.GetChange(base_branch, None)
4552 return owners_finder.OwnersFinder(
4553 [f.LocalPath() for f in
4554 cl.GetChange(base_branch, None).AffectedFiles()],
4555 change.RepositoryRoot(), author,
4556 fopen=file, os_path=os.path, glob=glob.glob,
4557 disable_color=options.no_color).run()
4558
4559
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004560def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004561 """Generates a diff command."""
4562 # Generate diff for the current branch's changes.
4563 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4564 upstream_commit, '--' ]
4565
4566 if args:
4567 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004568 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004569 diff_cmd.append(arg)
4570 else:
4571 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004572
4573 return diff_cmd
4574
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004575def MatchingFileType(file_name, extensions):
4576 """Returns true if the file name ends with one of the given extensions."""
4577 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004578
enne@chromium.org555cfe42014-01-29 18:21:39 +00004579@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004580def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004581 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00004582 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004583 GN_EXTS = ['.gn', '.gni']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004584 parser.add_option('--full', action='store_true',
4585 help='Reformat the full content of all touched files')
4586 parser.add_option('--dry-run', action='store_true',
4587 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004588 parser.add_option('--python', action='store_true',
4589 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004590 parser.add_option('--diff', action='store_true',
4591 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004592 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004593
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004594 # git diff generates paths against the root of the repository. Change
4595 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004596 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004597 if rel_base_path:
4598 os.chdir(rel_base_path)
4599
digit@chromium.org29e47272013-05-17 17:01:46 +00004600 # Grab the merge-base commit, i.e. the upstream commit of the current
4601 # branch when it was created or the last time it was rebased. This is
4602 # to cover the case where the user may have called "git fetch origin",
4603 # moving the origin branch to a newer commit, but hasn't rebased yet.
4604 upstream_commit = None
4605 cl = Changelist()
4606 upstream_branch = cl.GetUpstreamBranch()
4607 if upstream_branch:
4608 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4609 upstream_commit = upstream_commit.strip()
4610
4611 if not upstream_commit:
4612 DieWithError('Could not find base commit for this branch. '
4613 'Are you in detached state?')
4614
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004615 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4616 diff_output = RunGit(changed_files_cmd)
4617 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004618 # Filter out files deleted by this CL
4619 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004620
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004621 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4622 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4623 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004624 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004625
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004626 top_dir = os.path.normpath(
4627 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
4628
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004629 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4630 # formatted. This is used to block during the presubmit.
4631 return_value = 0
4632
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004633 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004634 # Locate the clang-format binary in the checkout
4635 try:
4636 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
4637 except clang_format.NotFoundError, e:
4638 DieWithError(e)
4639
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004640 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004641 cmd = [clang_format_tool]
4642 if not opts.dry_run and not opts.diff:
4643 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004644 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004645 if opts.diff:
4646 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004647 else:
4648 env = os.environ.copy()
4649 env['PATH'] = str(os.path.dirname(clang_format_tool))
4650 try:
4651 script = clang_format.FindClangFormatScriptInChromiumTree(
4652 'clang-format-diff.py')
4653 except clang_format.NotFoundError, e:
4654 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004655
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004656 cmd = [sys.executable, script, '-p0']
4657 if not opts.dry_run and not opts.diff:
4658 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004659
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004660 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
4661 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004662
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004663 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
4664 if opts.diff:
4665 sys.stdout.write(stdout)
4666 if opts.dry_run and len(stdout) > 0:
4667 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004668
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004669 # Similar code to above, but using yapf on .py files rather than clang-format
4670 # on C/C++ files
4671 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004672 yapf_tool = gclient_utils.FindExecutable('yapf')
4673 if yapf_tool is None:
4674 DieWithError('yapf not found in PATH')
4675
4676 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004677 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004678 cmd = [yapf_tool]
4679 if not opts.dry_run and not opts.diff:
4680 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004681 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004682 if opts.diff:
4683 sys.stdout.write(stdout)
4684 else:
4685 # TODO(sbc): yapf --lines mode still has some issues.
4686 # https://github.com/google/yapf/issues/154
4687 DieWithError('--python currently only works with --full')
4688
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004689 # Dart's formatter does not have the nice property of only operating on
4690 # modified chunks, so hard code full.
4691 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004692 try:
4693 command = [dart_format.FindDartFmtToolInChromiumTree()]
4694 if not opts.dry_run and not opts.diff:
4695 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004696 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004697
ppi@chromium.org6593d932016-03-03 15:41:15 +00004698 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004699 if opts.dry_run and stdout:
4700 return_value = 2
4701 except dart_format.NotFoundError as e:
erikcorry@chromium.org3e445022015-12-17 09:07:26 +00004702 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
4703 'found in this checkout. Files in other languages are still ' +
4704 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004705
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004706 # Format GN build files. Always run on full build files for canonical form.
4707 if gn_diff_files:
4708 cmd = ['gn', 'format']
4709 if not opts.dry_run and not opts.diff:
4710 cmd.append('--in-place')
4711 for gn_diff_file in gn_diff_files:
4712 stdout = RunCommand(cmd + [gn_diff_file], cwd=top_dir)
4713 if opts.diff:
4714 sys.stdout.write(stdout)
4715
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004716 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004717
4718
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004719@subcommand.usage('<codereview url or issue id>')
4720def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004721 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004722 _, args = parser.parse_args(args)
4723
4724 if len(args) != 1:
4725 parser.print_help()
4726 return 1
4727
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004728 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00004729 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004730 parser.print_help()
4731 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00004732 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004733
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004734 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00004735 output = RunGit(['config', '--local', '--get-regexp',
4736 r'branch\..*\.%s' % issueprefix],
4737 error_ok=True)
4738 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004739 if issue == target_issue:
4740 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004741
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004742 branches = []
4743 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00004744 branches.extend(find_issues(cls.IssueSettingSuffix()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004745 if len(branches) == 0:
4746 print 'No branch found for issue %s.' % target_issue
4747 return 1
4748 if len(branches) == 1:
4749 RunGit(['checkout', branches[0]])
4750 else:
4751 print 'Multiple branches match issue %s:' % target_issue
4752 for i in range(len(branches)):
4753 print '%d: %s' % (i, branches[i])
4754 which = raw_input('Choose by index: ')
4755 try:
4756 RunGit(['checkout', branches[int(which)]])
4757 except (IndexError, ValueError):
4758 print 'Invalid selection, not checking out any branch.'
4759 return 1
4760
4761 return 0
4762
4763
maruel@chromium.org29404b52014-09-08 22:58:00 +00004764def CMDlol(parser, args):
4765 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00004766 print zlib.decompress(base64.b64decode(
4767 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
4768 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
4769 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
4770 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00004771 return 0
4772
4773
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004774class OptionParser(optparse.OptionParser):
4775 """Creates the option parse and add --verbose support."""
4776 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004777 optparse.OptionParser.__init__(
4778 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004779 self.add_option(
4780 '-v', '--verbose', action='count', default=0,
4781 help='Use 2 times for more debugging info')
4782
4783 def parse_args(self, args=None, values=None):
4784 options, args = optparse.OptionParser.parse_args(self, args, values)
4785 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
4786 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
4787 return options, args
4788
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004789
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004790def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00004791 if sys.hexversion < 0x02060000:
4792 print >> sys.stderr, (
4793 '\nYour python version %s is unsupported, please upgrade.\n' %
4794 sys.version.split(' ', 1)[0])
4795 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004796
maruel@chromium.orgddd59412011-11-30 14:20:38 +00004797 # Reload settings.
4798 global settings
4799 settings = Settings()
4800
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004801 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004802 dispatcher = subcommand.CommandDispatcher(__name__)
4803 try:
4804 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00004805 except auth.AuthenticationError as e:
4806 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004807 except urllib2.HTTPError, e:
4808 if e.code != 500:
4809 raise
4810 DieWithError(
4811 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
4812 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00004813 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004814
4815
4816if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004817 # These affect sys.stdout so do it outside of main() to simplify mocks in
4818 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00004819 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004820 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00004821 try:
4822 sys.exit(main(sys.argv[1:]))
4823 except KeyboardInterrupt:
4824 sys.stderr.write('interrupted\n')
4825 sys.exit(1)