blob: bb31fa0ad9705de247d21e310e4e7f4fe16d7e27 [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.org013a2802016-03-29 09:52:33 +00002164 data = self._GetChangeDetail(['COMMIT_FOOTERS', 'CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002165 return data['revisions'][data['current_revision']]['commit_with_footers']
2166
2167 def UpdateDescriptionRemote(self, description):
2168 # TODO(tandrii)
2169 raise NotImplementedError()
2170
2171 def CloseIssue(self):
2172 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2173
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002174 def SubmitIssue(self, wait_for_merge=True):
2175 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2176 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002177
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002178 def _GetChangeDetail(self, options=None, issue=None):
2179 options = options or []
2180 issue = issue or self.GetIssue()
2181 assert issue, 'issue required to query Gerrit'
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002182 return gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2183 options)
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002184
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002185 def CMDLand(self, force, bypass_hooks, verbose):
2186 if git_common.is_dirty_git_tree('land'):
2187 return 1
2188 differs = True
2189 last_upload = RunGit(['config',
2190 'branch.%s.gerritsquashhash' % self.GetBranch()],
2191 error_ok=True).strip()
2192 # Note: git diff outputs nothing if there is no diff.
2193 if not last_upload or RunGit(['diff', last_upload]).strip():
2194 print('WARNING: some changes from local branch haven\'t been uploaded')
2195 else:
2196 detail = self._GetChangeDetail(['CURRENT_REVISION'])
2197 if detail['current_revision'] == last_upload:
2198 differs = False
2199 else:
2200 print('WARNING: local branch contents differ from latest uploaded '
2201 'patchset')
2202 if differs:
2203 if not force:
2204 ask_for_data(
2205 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2206 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2207 elif not bypass_hooks:
2208 hook_results = self.RunHook(
2209 committing=True,
2210 may_prompt=not force,
2211 verbose=verbose,
2212 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2213 if not hook_results.should_continue():
2214 return 1
2215
2216 self.SubmitIssue(wait_for_merge=True)
2217 print('Issue %s has been submitted.' % self.GetIssueURL())
2218 return 0
2219
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002220 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2221 directory):
2222 assert not reject
2223 assert not nocommit
2224 assert not directory
2225 assert parsed_issue_arg.valid
2226
2227 self._changelist.issue = parsed_issue_arg.issue
2228
2229 if parsed_issue_arg.hostname:
2230 self._gerrit_host = parsed_issue_arg.hostname
2231 self._gerrit_server = 'https://%s' % self._gerrit_host
2232
2233 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2234
2235 if not parsed_issue_arg.patchset:
2236 # Use current revision by default.
2237 revision_info = detail['revisions'][detail['current_revision']]
2238 patchset = int(revision_info['_number'])
2239 else:
2240 patchset = parsed_issue_arg.patchset
2241 for revision_info in detail['revisions'].itervalues():
2242 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2243 break
2244 else:
2245 DieWithError('Couldn\'t find patchset %i in issue %i' %
2246 (parsed_issue_arg.patchset, self.GetIssue()))
2247
2248 fetch_info = revision_info['fetch']['http']
2249 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2250 RunGit(['cherry-pick', 'FETCH_HEAD'])
2251 self.SetIssue(self.GetIssue())
2252 self.SetPatchset(patchset)
2253 print('Committed patch for issue %i pathset %i locally' %
2254 (self.GetIssue(), self.GetPatchset()))
2255 return 0
2256
2257 @staticmethod
2258 def ParseIssueURL(parsed_url):
2259 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2260 return None
2261 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2262 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2263 # Short urls like https://domain/<issue_number> can be used, but don't allow
2264 # specifying the patchset (you'd 404), but we allow that here.
2265 if parsed_url.path == '/':
2266 part = parsed_url.fragment
2267 else:
2268 part = parsed_url.path
2269 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2270 if match:
2271 return _ParsedIssueNumberArgument(
2272 issue=int(match.group(2)),
2273 patchset=int(match.group(4)) if match.group(4) else None,
2274 hostname=parsed_url.netloc)
2275 return None
2276
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002277 def CMDUploadChange(self, options, args, change):
2278 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002279 if options.squash and options.no_squash:
2280 DieWithError('Can only use one of --squash or --no-squash')
2281 options.squash = ((settings.GetSquashGerritUploads() or options.squash) and
2282 not options.no_squash)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002283 # We assume the remote called "origin" is the one we want.
2284 # It is probably not worthwhile to support different workflows.
2285 gerrit_remote = 'origin'
2286
2287 remote, remote_branch = self.GetRemoteBranch()
2288 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2289 pending_prefix='')
2290
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002291 if options.squash:
2292 if not self.GetIssue():
2293 # TODO(tandrii): deperecate this after 2016Q2. Backwards compatibility
2294 # with shadow branch, which used to contain change-id for a given
2295 # branch, using which we can fetch actual issue number and set it as the
2296 # property of the branch, which is the new way.
2297 message = RunGitSilent([
2298 'show', '--format=%B', '-s',
2299 'refs/heads/git_cl_uploads/%s' % self.GetBranch()])
2300 if message:
2301 change_ids = git_footers.get_footer_change_id(message.strip())
2302 if change_ids and len(change_ids) == 1:
2303 details = self._GetChangeDetail(issue=change_ids[0])
2304 if details:
2305 print('WARNING: found old upload in branch git_cl_uploads/%s '
2306 'corresponding to issue %s' %
2307 (self.GetBranch(), details['_number']))
2308 self.SetIssue(details['_number'])
2309 if not self.GetIssue():
2310 DieWithError(
2311 '\n' # For readability of the blob below.
2312 'Found old upload in branch git_cl_uploads/%s, '
2313 'but failed to find corresponding Gerrit issue.\n'
2314 'If you know the issue number, set it manually first:\n'
2315 ' git cl issue 123456\n'
2316 'If you intended to upload this CL as new issue, '
2317 'just delete or rename the old upload branch:\n'
2318 ' git rename-branch git_cl_uploads/%s old_upload-%s\n'
2319 'After that, please run git cl upload again.' %
2320 tuple([self.GetBranch()] * 3))
2321 # End of backwards compatability.
2322
2323 if self.GetIssue():
2324 # Try to get the message from a previous upload.
2325 message = self.GetDescription()
2326 if not message:
2327 DieWithError(
2328 'failed to fetch description from current Gerrit issue %d\n'
2329 '%s' % (self.GetIssue(), self.GetIssueURL()))
2330 change_id = self._GetChangeDetail()['change_id']
2331 while True:
2332 footer_change_ids = git_footers.get_footer_change_id(message)
2333 if footer_change_ids == [change_id]:
2334 break
2335 if not footer_change_ids:
2336 message = git_footers.add_footer_change_id(message, change_id)
2337 print('WARNING: appended missing Change-Id to issue description')
2338 continue
2339 # There is already a valid footer but with different or several ids.
2340 # Doing this automatically is non-trivial as we don't want to lose
2341 # existing other footers, yet we want to append just 1 desired
2342 # Change-Id. Thus, just create a new footer, but let user verify the
2343 # new description.
2344 message = '%s\n\nChange-Id: %s' % (message, change_id)
2345 print(
2346 'WARNING: issue %s has Change-Id footer(s):\n'
2347 ' %s\n'
2348 'but issue has Change-Id %s, according to Gerrit.\n'
2349 'Please, check the proposed correction to the description, '
2350 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2351 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2352 change_id))
2353 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2354 if not options.force:
2355 change_desc = ChangeDescription(message)
2356 change_desc.prompt()
2357 message = change_desc.description
2358 if not message:
2359 DieWithError("Description is empty. Aborting...")
2360 # Continue the while loop.
2361 # Sanity check of this code - we should end up with proper message
2362 # footer.
2363 assert [change_id] == git_footers.get_footer_change_id(message)
2364 change_desc = ChangeDescription(message)
2365 else:
2366 change_desc = ChangeDescription(
2367 options.message or CreateDescriptionFromLog(args))
2368 if not options.force:
2369 change_desc.prompt()
2370 if not change_desc.description:
2371 DieWithError("Description is empty. Aborting...")
2372 message = change_desc.description
2373 change_ids = git_footers.get_footer_change_id(message)
2374 if len(change_ids) > 1:
2375 DieWithError('too many Change-Id footers, at most 1 allowed.')
2376 if not change_ids:
2377 # Generate the Change-Id automatically.
2378 message = git_footers.add_footer_change_id(
2379 message, GenerateGerritChangeId(message))
2380 change_desc.set_description(message)
2381 change_ids = git_footers.get_footer_change_id(message)
2382 assert len(change_ids) == 1
2383 change_id = change_ids[0]
2384
2385 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2386 if remote is '.':
2387 # If our upstream branch is local, we base our squashed commit on its
2388 # squashed version.
2389 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2390 # Check the squashed hash of the parent.
2391 parent = RunGit(['config',
2392 'branch.%s.gerritsquashhash' % upstream_branch_name],
2393 error_ok=True).strip()
2394 # Verify that the upstream branch has been uploaded too, otherwise
2395 # Gerrit will create additional CLs when uploading.
2396 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2397 RunGitSilent(['rev-parse', parent + ':'])):
2398 # TODO(tandrii): remove "old depot_tools" part on April 12, 2016.
2399 DieWithError(
2400 'Upload upstream branch %s first.\n'
2401 'Note: maybe you\'ve uploaded it with --no-squash or with an old '
2402 'version of depot_tools. If so, then re-upload it with:\n'
2403 ' git cl upload --squash\n' % upstream_branch_name)
2404 else:
2405 parent = self.GetCommonAncestorWithUpstream()
2406
2407 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2408 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2409 '-m', message]).strip()
2410 else:
2411 change_desc = ChangeDescription(
2412 options.message or CreateDescriptionFromLog(args))
2413 if not change_desc.description:
2414 DieWithError("Description is empty. Aborting...")
2415
2416 if not git_footers.get_footer_change_id(change_desc.description):
2417 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002418 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2419 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002420 ref_to_push = 'HEAD'
2421 parent = '%s/%s' % (gerrit_remote, branch)
2422 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2423
2424 assert change_desc
2425 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2426 ref_to_push)]).splitlines()
2427 if len(commits) > 1:
2428 print('WARNING: This will upload %d commits. Run the following command '
2429 'to see which commits will be uploaded: ' % len(commits))
2430 print('git log %s..%s' % (parent, ref_to_push))
2431 print('You can also use `git squash-branch` to squash these into a '
2432 'single commit.')
2433 ask_for_data('About to upload; enter to confirm.')
2434
2435 if options.reviewers or options.tbr_owners:
2436 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2437 change)
2438
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002439 # Extra options that can be specified at push time. Doc:
2440 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2441 refspec_opts = []
2442 if options.title:
2443 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2444 # reverse on its side.
2445 if '_' in options.title:
2446 print('WARNING: underscores in title will be converted to spaces.')
2447 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2448
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002449 cc = self.GetCCList().split(',')
2450 if options.cc:
2451 cc.extend(options.cc)
2452 cc = filter(None, cc)
2453 if cc:
tandrii@chromium.org0b2d7072016-04-18 16:19:03 +00002454 # refspec_opts.extend('cc=' + email.strip() for email in cc)
2455 # TODO(tandrii): enable this back. http://crbug.com/604377
2456 print('WARNING: Gerrit doesn\'t yet support cc-ing arbitrary emails.\n'
2457 ' Ignoring cc-ed emails. See http://crbug.com/604377.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002458
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002459 if change_desc.get_reviewers():
2460 refspec_opts.extend('r=' + email.strip()
2461 for email in change_desc.get_reviewers())
2462
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002463
2464 refspec_suffix = ''
2465 if refspec_opts:
2466 refspec_suffix = '%' + ','.join(refspec_opts)
2467 assert ' ' not in refspec_suffix, (
2468 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002469 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002470
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002471 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002472 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002473 print_stdout=True,
2474 # Flush after every line: useful for seeing progress when running as
2475 # recipe.
2476 filter_fn=lambda _: sys.stdout.flush())
2477
2478 if options.squash:
2479 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2480 change_numbers = [m.group(1)
2481 for m in map(regex.match, push_stdout.splitlines())
2482 if m]
2483 if len(change_numbers) != 1:
2484 DieWithError(
2485 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2486 'Change-Id: %s') % (len(change_numbers), change_id))
2487 self.SetIssue(change_numbers[0])
2488 RunGit(['config', 'branch.%s.gerritsquashhash' % self.GetBranch(),
2489 ref_to_push])
2490 return 0
2491
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002492 def _AddChangeIdToCommitMessage(self, options, args):
2493 """Re-commits using the current message, assumes the commit hook is in
2494 place.
2495 """
2496 log_desc = options.message or CreateDescriptionFromLog(args)
2497 git_command = ['commit', '--amend', '-m', log_desc]
2498 RunGit(git_command)
2499 new_log_desc = CreateDescriptionFromLog(args)
2500 if git_footers.get_footer_change_id(new_log_desc):
2501 print 'git-cl: Added Change-Id to commit message.'
2502 return new_log_desc
2503 else:
2504 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002505
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002506 def SetCQState(self, new_state):
2507 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
2508 # TODO(tandrii): maybe allow configurability in codereview.settings or by
2509 # self-discovery of label config for this CL using REST API.
2510 vote_map = {
2511 _CQState.NONE: 0,
2512 _CQState.DRY_RUN: 1,
2513 _CQState.COMMIT : 2,
2514 }
2515 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2516 labels={'Commit-Queue': vote_map[new_state]})
2517
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002518
2519_CODEREVIEW_IMPLEMENTATIONS = {
2520 'rietveld': _RietveldChangelistImpl,
2521 'gerrit': _GerritChangelistImpl,
2522}
2523
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002524
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002525def _add_codereview_select_options(parser):
2526 """Appends --gerrit and --rietveld options to force specific codereview."""
2527 parser.codereview_group = optparse.OptionGroup(
2528 parser, 'EXPERIMENTAL! Codereview override options')
2529 parser.add_option_group(parser.codereview_group)
2530 parser.codereview_group.add_option(
2531 '--gerrit', action='store_true',
2532 help='Force the use of Gerrit for codereview')
2533 parser.codereview_group.add_option(
2534 '--rietveld', action='store_true',
2535 help='Force the use of Rietveld for codereview')
2536
2537
2538def _process_codereview_select_options(parser, options):
2539 if options.gerrit and options.rietveld:
2540 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2541 options.forced_codereview = None
2542 if options.gerrit:
2543 options.forced_codereview = 'gerrit'
2544 elif options.rietveld:
2545 options.forced_codereview = 'rietveld'
2546
2547
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002548class ChangeDescription(object):
2549 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002550 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002551 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002552
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002553 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002554 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002555
agable@chromium.org42c20792013-09-12 17:34:49 +00002556 @property # www.logilab.org/ticket/89786
2557 def description(self): # pylint: disable=E0202
2558 return '\n'.join(self._description_lines)
2559
2560 def set_description(self, desc):
2561 if isinstance(desc, basestring):
2562 lines = desc.splitlines()
2563 else:
2564 lines = [line.rstrip() for line in desc]
2565 while lines and not lines[0]:
2566 lines.pop(0)
2567 while lines and not lines[-1]:
2568 lines.pop(-1)
2569 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002570
piman@chromium.org336f9122014-09-04 02:16:55 +00002571 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002572 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002573 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002574 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002575 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002576 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002577
agable@chromium.org42c20792013-09-12 17:34:49 +00002578 # Get the set of R= and TBR= lines and remove them from the desciption.
2579 regexp = re.compile(self.R_LINE)
2580 matches = [regexp.match(line) for line in self._description_lines]
2581 new_desc = [l for i, l in enumerate(self._description_lines)
2582 if not matches[i]]
2583 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002584
agable@chromium.org42c20792013-09-12 17:34:49 +00002585 # Construct new unified R= and TBR= lines.
2586 r_names = []
2587 tbr_names = []
2588 for match in matches:
2589 if not match:
2590 continue
2591 people = cleanup_list([match.group(2).strip()])
2592 if match.group(1) == 'TBR':
2593 tbr_names.extend(people)
2594 else:
2595 r_names.extend(people)
2596 for name in r_names:
2597 if name not in reviewers:
2598 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002599 if add_owners_tbr:
2600 owners_db = owners.Database(change.RepositoryRoot(),
2601 fopen=file, os_path=os.path, glob=glob.glob)
2602 all_reviewers = set(tbr_names + reviewers)
2603 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2604 all_reviewers)
2605 tbr_names.extend(owners_db.reviewers_for(missing_files,
2606 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002607 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2608 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2609
2610 # Put the new lines in the description where the old first R= line was.
2611 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2612 if 0 <= line_loc < len(self._description_lines):
2613 if new_tbr_line:
2614 self._description_lines.insert(line_loc, new_tbr_line)
2615 if new_r_line:
2616 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002617 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002618 if new_r_line:
2619 self.append_footer(new_r_line)
2620 if new_tbr_line:
2621 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002622
2623 def prompt(self):
2624 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002625 self.set_description([
2626 '# Enter a description of the change.',
2627 '# This will be displayed on the codereview site.',
2628 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002629 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002630 '--------------------',
2631 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002632
agable@chromium.org42c20792013-09-12 17:34:49 +00002633 regexp = re.compile(self.BUG_LINE)
2634 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00002635 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00002636 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002637 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002638 if not content:
2639 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002640 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002641
2642 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002643 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2644 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002645 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002646 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002647
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002648 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00002649 if self._description_lines:
2650 # Add an empty line if either the last line or the new line isn't a tag.
2651 last_line = self._description_lines[-1]
2652 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
2653 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2654 self._description_lines.append('')
2655 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002656
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002657 def get_reviewers(self):
2658 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002659 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
2660 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002661 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002662
2663
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002664def get_approving_reviewers(props):
2665 """Retrieves the reviewers that approved a CL from the issue properties with
2666 messages.
2667
2668 Note that the list may contain reviewers that are not committer, thus are not
2669 considered by the CQ.
2670 """
2671 return sorted(
2672 set(
2673 message['sender']
2674 for message in props['messages']
2675 if message['approval'] and message['sender'] in props['reviewers']
2676 )
2677 )
2678
2679
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002680def FindCodereviewSettingsFile(filename='codereview.settings'):
2681 """Finds the given file starting in the cwd and going up.
2682
2683 Only looks up to the top of the repository unless an
2684 'inherit-review-settings-ok' file exists in the root of the repository.
2685 """
2686 inherit_ok_file = 'inherit-review-settings-ok'
2687 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002688 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002689 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2690 root = '/'
2691 while True:
2692 if filename in os.listdir(cwd):
2693 if os.path.isfile(os.path.join(cwd, filename)):
2694 return open(os.path.join(cwd, filename))
2695 if cwd == root:
2696 break
2697 cwd = os.path.dirname(cwd)
2698
2699
2700def LoadCodereviewSettingsFromFile(fileobj):
2701 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002702 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002703
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002704 def SetProperty(name, setting, unset_error_ok=False):
2705 fullname = 'rietveld.' + name
2706 if setting in keyvals:
2707 RunGit(['config', fullname, keyvals[setting]])
2708 else:
2709 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2710
2711 SetProperty('server', 'CODE_REVIEW_SERVER')
2712 # Only server setting is required. Other settings can be absent.
2713 # In that case, we ignore errors raised during option deletion attempt.
2714 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002715 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002716 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2717 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002718 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002719 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002720 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2721 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002722 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002723 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002724 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002725 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2726 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002727
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002728 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002729 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002730
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002731 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
2732 RunGit(['config', 'gerrit.squash-uploads',
2733 keyvals['GERRIT_SQUASH_UPLOADS']])
2734
tandrii@chromium.org28253532016-04-14 13:46:56 +00002735 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00002736 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00002737 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
2738
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002739 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2740 #should be of the form
2741 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2742 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2743 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2744 keyvals['ORIGIN_URL_CONFIG']])
2745
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002746
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002747def urlretrieve(source, destination):
2748 """urllib is broken for SSL connections via a proxy therefore we
2749 can't use urllib.urlretrieve()."""
2750 with open(destination, 'w') as f:
2751 f.write(urllib2.urlopen(source).read())
2752
2753
ukai@chromium.org712d6102013-11-27 00:52:58 +00002754def hasSheBang(fname):
2755 """Checks fname is a #! script."""
2756 with open(fname) as f:
2757 return f.read(2).startswith('#!')
2758
2759
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00002760# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
2761def DownloadHooks(*args, **kwargs):
2762 pass
2763
2764
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002765def DownloadGerritHook(force):
2766 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002767
2768 Args:
2769 force: True to update hooks. False to install hooks if not present.
2770 """
2771 if not settings.GetIsGerrit():
2772 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00002773 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002774 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2775 if not os.access(dst, os.X_OK):
2776 if os.path.exists(dst):
2777 if not force:
2778 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002779 try:
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002780 print(
2781 'WARNING: installing Gerrit commit-msg hook.\n'
2782 ' This behavior of git cl will soon be disabled.\n'
2783 ' See bug http://crbug.com/579176.')
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002784 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002785 if not hasSheBang(dst):
2786 DieWithError('Not a script: %s\n'
2787 'You need to download from\n%s\n'
2788 'into .git/hooks/commit-msg and '
2789 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002790 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2791 except Exception:
2792 if os.path.exists(dst):
2793 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002794 DieWithError('\nFailed to download hooks.\n'
2795 'You need to download from\n%s\n'
2796 'into .git/hooks/commit-msg and '
2797 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002798
2799
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002800
2801def GetRietveldCodereviewSettingsInteractively():
2802 """Prompt the user for settings."""
2803 server = settings.GetDefaultServerUrl(error_ok=True)
2804 prompt = 'Rietveld server (host[:port])'
2805 prompt += ' [%s]' % (server or DEFAULT_SERVER)
2806 newserver = ask_for_data(prompt + ':')
2807 if not server and not newserver:
2808 newserver = DEFAULT_SERVER
2809 if newserver:
2810 newserver = gclient_utils.UpgradeToHttps(newserver)
2811 if newserver != server:
2812 RunGit(['config', 'rietveld.server', newserver])
2813
2814 def SetProperty(initial, caption, name, is_url):
2815 prompt = caption
2816 if initial:
2817 prompt += ' ("x" to clear) [%s]' % initial
2818 new_val = ask_for_data(prompt + ':')
2819 if new_val == 'x':
2820 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
2821 elif new_val:
2822 if is_url:
2823 new_val = gclient_utils.UpgradeToHttps(new_val)
2824 if new_val != initial:
2825 RunGit(['config', 'rietveld.' + name, new_val])
2826
2827 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
2828 SetProperty(settings.GetDefaultPrivateFlag(),
2829 'Private flag (rietveld only)', 'private', False)
2830 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
2831 'tree-status-url', False)
2832 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
2833 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
2834 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
2835 'run-post-upload-hook', False)
2836
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002837@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002838def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002839 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002840
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002841 print('WARNING: git cl config works for Rietveld only.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002842 'For Gerrit, see http://crbug.com/603116.')
2843 # TODO(tandrii): add Gerrit support as part of http://crbug.com/603116.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00002844 parser.add_option('--activate-update', action='store_true',
2845 help='activate auto-updating [rietveld] section in '
2846 '.git/config')
2847 parser.add_option('--deactivate-update', action='store_true',
2848 help='deactivate auto-updating [rietveld] section in '
2849 '.git/config')
2850 options, args = parser.parse_args(args)
2851
2852 if options.deactivate_update:
2853 RunGit(['config', 'rietveld.autoupdate', 'false'])
2854 return
2855
2856 if options.activate_update:
2857 RunGit(['config', '--unset', 'rietveld.autoupdate'])
2858 return
2859
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002860 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002861 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002862 return 0
2863
2864 url = args[0]
2865 if not url.endswith('codereview.settings'):
2866 url = os.path.join(url, 'codereview.settings')
2867
2868 # Load code review settings and download hooks (if available).
2869 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
2870 return 0
2871
2872
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002873def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002874 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002875 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
2876 branch = ShortBranchName(branchref)
2877 _, args = parser.parse_args(args)
2878 if not args:
2879 print("Current base-url:")
2880 return RunGit(['config', 'branch.%s.base-url' % branch],
2881 error_ok=False).strip()
2882 else:
2883 print("Setting base-url to %s" % args[0])
2884 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
2885 error_ok=False).strip()
2886
2887
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002888def color_for_status(status):
2889 """Maps a Changelist status to color, for CMDstatus and other tools."""
2890 return {
2891 'unsent': Fore.RED,
2892 'waiting': Fore.BLUE,
2893 'reply': Fore.YELLOW,
2894 'lgtm': Fore.GREEN,
2895 'commit': Fore.MAGENTA,
2896 'closed': Fore.CYAN,
2897 'error': Fore.WHITE,
2898 }.get(status, Fore.WHITE)
2899
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002900def get_cl_statuses(
clemensh@chromium.org81156e62016-04-25 08:22:13 +00002901 changes, fine_grained, max_processes=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002902 """Returns a blocking iterable of (branch, issue, color) for given branches.
2903
2904 If fine_grained is true, this will fetch CL statuses from the server.
2905 Otherwise, simply indicate if there's a matching url for the given branches.
2906
2907 If max_processes is specified, it is used as the maximum number of processes
2908 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
2909 spawned.
2910 """
2911 # Silence upload.py otherwise it becomes unwieldly.
2912 upload.verbosity = 0
2913
2914 if fine_grained:
2915 # Process one branch synchronously to work through authentication, then
2916 # spawn processes to process all the other branches in parallel.
clemensh@chromium.org81156e62016-04-25 08:22:13 +00002917 if changes:
2918 fetch = lambda cl: (cl, cl.GetStatus())
2919 yield fetch(changes[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002920
clemensh@chromium.org81156e62016-04-25 08:22:13 +00002921 changes_to_fetch = changes[1:]
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002922 pool = ThreadPool(
clemensh@chromium.org81156e62016-04-25 08:22:13 +00002923 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002924 if max_processes is not None
clemensh@chromium.org81156e62016-04-25 08:22:13 +00002925 else len(changes_to_fetch))
2926 for x in pool.imap_unordered(fetch, changes_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002927 yield x
2928 else:
2929 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.org81156e62016-04-25 08:22:13 +00002930 for cl in changes:
2931 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002932
rmistry@google.com2dd99862015-06-22 12:22:18 +00002933
2934def upload_branch_deps(cl, args):
2935 """Uploads CLs of local branches that are dependents of the current branch.
2936
2937 If the local branch dependency tree looks like:
2938 test1 -> test2.1 -> test3.1
2939 -> test3.2
2940 -> test2.2 -> test3.3
2941
2942 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
2943 run on the dependent branches in this order:
2944 test2.1, test3.1, test3.2, test2.2, test3.3
2945
2946 Note: This function does not rebase your local dependent branches. Use it when
2947 you make a change to the parent branch that will not conflict with its
2948 dependent branches, and you would like their dependencies updated in
2949 Rietveld.
2950 """
2951 if git_common.is_dirty_git_tree('upload-branch-deps'):
2952 return 1
2953
2954 root_branch = cl.GetBranch()
2955 if root_branch is None:
2956 DieWithError('Can\'t find dependent branches from detached HEAD state. '
2957 'Get on a branch!')
2958 if not cl.GetIssue() or not cl.GetPatchset():
2959 DieWithError('Current branch does not have an uploaded CL. We cannot set '
2960 'patchset dependencies without an uploaded CL.')
2961
2962 branches = RunGit(['for-each-ref',
2963 '--format=%(refname:short) %(upstream:short)',
2964 'refs/heads'])
2965 if not branches:
2966 print('No local branches found.')
2967 return 0
2968
2969 # Create a dictionary of all local branches to the branches that are dependent
2970 # on it.
2971 tracked_to_dependents = collections.defaultdict(list)
2972 for b in branches.splitlines():
2973 tokens = b.split()
2974 if len(tokens) == 2:
2975 branch_name, tracked = tokens
2976 tracked_to_dependents[tracked].append(branch_name)
2977
2978 print
2979 print 'The dependent local branches of %s are:' % root_branch
2980 dependents = []
2981 def traverse_dependents_preorder(branch, padding=''):
2982 dependents_to_process = tracked_to_dependents.get(branch, [])
2983 padding += ' '
2984 for dependent in dependents_to_process:
2985 print '%s%s' % (padding, dependent)
2986 dependents.append(dependent)
2987 traverse_dependents_preorder(dependent, padding)
2988 traverse_dependents_preorder(root_branch)
2989 print
2990
2991 if not dependents:
2992 print 'There are no dependent local branches for %s' % root_branch
2993 return 0
2994
2995 print ('This command will checkout all dependent branches and run '
2996 '"git cl upload".')
2997 ask_for_data('[Press enter to continue or ctrl-C to quit]')
2998
andybons@chromium.org962f9462016-02-03 20:00:42 +00002999 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003000 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003001 args.extend(['-t', 'Updated patchset dependency'])
3002
rmistry@google.com2dd99862015-06-22 12:22:18 +00003003 # Record all dependents that failed to upload.
3004 failures = {}
3005 # Go through all dependents, checkout the branch and upload.
3006 try:
3007 for dependent_branch in dependents:
3008 print
3009 print '--------------------------------------'
3010 print 'Running "git cl upload" from %s:' % dependent_branch
3011 RunGit(['checkout', '-q', dependent_branch])
3012 print
3013 try:
3014 if CMDupload(OptionParser(), args) != 0:
3015 print 'Upload failed for %s!' % dependent_branch
3016 failures[dependent_branch] = 1
3017 except: # pylint: disable=W0702
3018 failures[dependent_branch] = 1
3019 print
3020 finally:
3021 # Swap back to the original root branch.
3022 RunGit(['checkout', '-q', root_branch])
3023
3024 print
3025 print 'Upload complete for dependent branches!'
3026 for dependent_branch in dependents:
3027 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
3028 print ' %s : %s' % (dependent_branch, upload_status)
3029 print
3030
3031 return 0
3032
3033
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003034def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003035 """Show status of changelists.
3036
3037 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003038 - Red not sent for review or broken
3039 - Blue waiting for review
3040 - Yellow waiting for you to reply to review
3041 - Green LGTM'ed
3042 - Magenta in the commit queue
3043 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003044
3045 Also see 'git cl comments'.
3046 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003047 parser.add_option('--field',
3048 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003049 parser.add_option('-f', '--fast', action='store_true',
3050 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003051 parser.add_option(
3052 '-j', '--maxjobs', action='store', type=int,
3053 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003054
3055 auth.add_auth_options(parser)
3056 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003057 if args:
3058 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003059 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003060
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003061 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003062 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003063 if options.field.startswith('desc'):
3064 print cl.GetDescription()
3065 elif options.field == 'id':
3066 issueid = cl.GetIssue()
3067 if issueid:
3068 print issueid
3069 elif options.field == 'patch':
3070 patchset = cl.GetPatchset()
3071 if patchset:
3072 print patchset
3073 elif options.field == 'url':
3074 url = cl.GetIssueURL()
3075 if url:
3076 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003077 return 0
3078
3079 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3080 if not branches:
3081 print('No local branch found.')
3082 return 0
3083
clemensh@chromium.org81156e62016-04-25 08:22:13 +00003084 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003085 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.org81156e62016-04-25 08:22:13 +00003086 for b in branches.splitlines()]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003087 print 'Branches associated with reviews:'
clemensh@chromium.org81156e62016-04-25 08:22:13 +00003088 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003089 fine_grained=not options.fast,
clemensh@chromium.org81156e62016-04-25 08:22:13 +00003090 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003091
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003092 branch_statuses = {}
clemensh@chromium.org81156e62016-04-25 08:22:13 +00003093 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3094 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3095 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003096 while branch not in branch_statuses:
clemensh@chromium.org81156e62016-04-25 08:22:13 +00003097 c, status = output.next()
3098 branch_statuses[c.GetBranch()] = status
3099 status = branch_statuses.pop(branch)
3100 url = cl.GetIssueURL()
3101 if url and (not status or status == 'error'):
3102 # The issue probably doesn't exist anymore.
3103 url += ' (broken)'
3104
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003105 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003106 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003107 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003108 color = ''
3109 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003110 status_str = '(%s)' % status if status else ''
3111 print ' %*s : %s%s %s%s' % (
clemensh@chromium.org81156e62016-04-25 08:22:13 +00003112 alignment, ShortBranchName(branch), color, url,
3113 status_str, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003114
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003115 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003116 print
3117 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003118 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003119 if not cl.GetIssue():
3120 print 'No issue assigned.'
3121 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003122 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00003123 if not options.fast:
3124 print 'Issue description:'
3125 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003126 return 0
3127
3128
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003129def colorize_CMDstatus_doc():
3130 """To be called once in main() to add colors to git cl status help."""
3131 colors = [i for i in dir(Fore) if i[0].isupper()]
3132
3133 def colorize_line(line):
3134 for color in colors:
3135 if color in line.upper():
3136 # Extract whitespaces first and the leading '-'.
3137 indent = len(line) - len(line.lstrip(' ')) + 1
3138 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3139 return line
3140
3141 lines = CMDstatus.__doc__.splitlines()
3142 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3143
3144
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003145@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003146def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003147 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003148
3149 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003150 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003151 parser.add_option('-r', '--reverse', action='store_true',
3152 help='Lookup the branch(es) for the specified issues. If '
3153 'no issues are specified, all branches with mapped '
3154 'issues will be listed.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003155 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003156 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003157 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003158
dnj@chromium.org406c4402015-03-03 17:22:28 +00003159 if options.reverse:
3160 branches = RunGit(['for-each-ref', 'refs/heads',
3161 '--format=%(refname:short)']).splitlines()
3162
3163 # Reverse issue lookup.
3164 issue_branch_map = {}
3165 for branch in branches:
3166 cl = Changelist(branchref=branch)
3167 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3168 if not args:
3169 args = sorted(issue_branch_map.iterkeys())
3170 for issue in args:
3171 if not issue:
3172 continue
3173 print 'Branch for issue number %s: %s' % (
3174 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
3175 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003176 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003177 if len(args) > 0:
3178 try:
3179 issue = int(args[0])
3180 except ValueError:
3181 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003182 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003183 cl.SetIssue(issue)
3184 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003185 return 0
3186
3187
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003188def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003189 """Shows or posts review comments for any changelist."""
3190 parser.add_option('-a', '--add-comment', dest='comment',
3191 help='comment to add to an issue')
3192 parser.add_option('-i', dest='issue',
3193 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003194 parser.add_option('-j', '--json-file',
3195 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003196 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003197 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003198 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003199
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003200 issue = None
3201 if options.issue:
3202 try:
3203 issue = int(options.issue)
3204 except ValueError:
3205 DieWithError('A review issue id is expected to be a number')
3206
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003207 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003208
3209 if options.comment:
3210 cl.AddComment(options.comment)
3211 return 0
3212
3213 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003214 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003215 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003216 summary.append({
3217 'date': message['date'],
3218 'lgtm': False,
3219 'message': message['text'],
3220 'not_lgtm': False,
3221 'sender': message['sender'],
3222 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003223 if message['disapproval']:
3224 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003225 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003226 elif message['approval']:
3227 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003228 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003229 elif message['sender'] == data['owner_email']:
3230 color = Fore.MAGENTA
3231 else:
3232 color = Fore.BLUE
3233 print '\n%s%s %s%s' % (
3234 color, message['date'].split('.', 1)[0], message['sender'],
3235 Fore.RESET)
3236 if message['text'].strip():
3237 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00003238 if options.json_file:
3239 with open(options.json_file, 'wb') as f:
3240 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003241 return 0
3242
3243
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003244def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003245 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003246 parser.add_option('-d', '--display', action='store_true',
3247 help='Display the description instead of opening an editor')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003248 auth.add_auth_options(parser)
3249 options, _ = parser.parse_args(args)
3250 auth_config = auth.extract_auth_config_from_options(options)
3251 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003252 if not cl.GetIssue():
3253 DieWithError('This branch has no associated changelist.')
3254 description = ChangeDescription(cl.GetDescription())
smut@google.com34fb6b12015-07-13 20:03:26 +00003255 if options.display:
3256 print description.description
3257 return 0
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003258 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003259 if cl.GetDescription() != description.description:
3260 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003261 return 0
3262
3263
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003264def CreateDescriptionFromLog(args):
3265 """Pulls out the commit log to use as a base for the CL description."""
3266 log_args = []
3267 if len(args) == 1 and not args[0].endswith('.'):
3268 log_args = [args[0] + '..']
3269 elif len(args) == 1 and args[0].endswith('...'):
3270 log_args = [args[0][:-1]]
3271 elif len(args) == 2:
3272 log_args = [args[0] + '..' + args[1]]
3273 else:
3274 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003275 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003276
3277
thestig@chromium.org44202a22014-03-11 19:22:18 +00003278def CMDlint(parser, args):
3279 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003280 parser.add_option('--filter', action='append', metavar='-x,+y',
3281 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003282 auth.add_auth_options(parser)
3283 options, args = parser.parse_args(args)
3284 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003285
3286 # Access to a protected member _XX of a client class
3287 # pylint: disable=W0212
3288 try:
3289 import cpplint
3290 import cpplint_chromium
3291 except ImportError:
3292 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
3293 return 1
3294
3295 # Change the current working directory before calling lint so that it
3296 # shows the correct base.
3297 previous_cwd = os.getcwd()
3298 os.chdir(settings.GetRoot())
3299 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003300 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003301 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3302 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003303 if not files:
3304 print "Cannot lint an empty CL"
3305 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003306
3307 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003308 command = args + files
3309 if options.filter:
3310 command = ['--filter=' + ','.join(options.filter)] + command
3311 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003312
3313 white_regex = re.compile(settings.GetLintRegex())
3314 black_regex = re.compile(settings.GetLintIgnoreRegex())
3315 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3316 for filename in filenames:
3317 if white_regex.match(filename):
3318 if black_regex.match(filename):
3319 print "Ignoring file %s" % filename
3320 else:
3321 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3322 extra_check_functions)
3323 else:
3324 print "Skipping file %s" % filename
3325 finally:
3326 os.chdir(previous_cwd)
3327 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
3328 if cpplint._cpplint_state.error_count != 0:
3329 return 1
3330 return 0
3331
3332
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003333def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003334 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003335 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003336 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003337 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003338 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003339 auth.add_auth_options(parser)
3340 options, args = parser.parse_args(args)
3341 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003342
sbc@chromium.org71437c02015-04-09 19:29:40 +00003343 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00003344 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003345 return 1
3346
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003347 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003348 if args:
3349 base_branch = args[0]
3350 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003351 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003352 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003353
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003354 cl.RunHook(
3355 committing=not options.upload,
3356 may_prompt=False,
3357 verbose=options.verbose,
3358 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003359 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003360
3361
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003362def GenerateGerritChangeId(message):
3363 """Returns Ixxxxxx...xxx change id.
3364
3365 Works the same way as
3366 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3367 but can be called on demand on all platforms.
3368
3369 The basic idea is to generate git hash of a state of the tree, original commit
3370 message, author/committer info and timestamps.
3371 """
3372 lines = []
3373 tree_hash = RunGitSilent(['write-tree'])
3374 lines.append('tree %s' % tree_hash.strip())
3375 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3376 if code == 0:
3377 lines.append('parent %s' % parent.strip())
3378 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3379 lines.append('author %s' % author.strip())
3380 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3381 lines.append('committer %s' % committer.strip())
3382 lines.append('')
3383 # Note: Gerrit's commit-hook actually cleans message of some lines and
3384 # whitespace. This code is not doing this, but it clearly won't decrease
3385 # entropy.
3386 lines.append(message)
3387 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3388 stdin='\n'.join(lines))
3389 return 'I%s' % change_hash.strip()
3390
3391
wittman@chromium.org455dc922015-01-26 20:15:50 +00003392def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3393 """Computes the remote branch ref to use for the CL.
3394
3395 Args:
3396 remote (str): The git remote for the CL.
3397 remote_branch (str): The git remote branch for the CL.
3398 target_branch (str): The target branch specified by the user.
3399 pending_prefix (str): The pending prefix from the settings.
3400 """
3401 if not (remote and remote_branch):
3402 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003403
wittman@chromium.org455dc922015-01-26 20:15:50 +00003404 if target_branch:
3405 # Cannonicalize branch references to the equivalent local full symbolic
3406 # refs, which are then translated into the remote full symbolic refs
3407 # below.
3408 if '/' not in target_branch:
3409 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3410 else:
3411 prefix_replacements = (
3412 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3413 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3414 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3415 )
3416 match = None
3417 for regex, replacement in prefix_replacements:
3418 match = re.search(regex, target_branch)
3419 if match:
3420 remote_branch = target_branch.replace(match.group(0), replacement)
3421 break
3422 if not match:
3423 # This is a branch path but not one we recognize; use as-is.
3424 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003425 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3426 # Handle the refs that need to land in different refs.
3427 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003428
wittman@chromium.org455dc922015-01-26 20:15:50 +00003429 # Create the true path to the remote branch.
3430 # Does the following translation:
3431 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3432 # * refs/remotes/origin/master -> refs/heads/master
3433 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3434 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3435 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3436 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3437 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3438 'refs/heads/')
3439 elif remote_branch.startswith('refs/remotes/branch-heads'):
3440 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3441 # If a pending prefix exists then replace refs/ with it.
3442 if pending_prefix:
3443 remote_branch = remote_branch.replace('refs/', pending_prefix)
3444 return remote_branch
3445
3446
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003447def cleanup_list(l):
3448 """Fixes a list so that comma separated items are put as individual items.
3449
3450 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3451 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3452 """
3453 items = sum((i.split(',') for i in l), [])
3454 stripped_items = (i.strip() for i in items)
3455 return sorted(filter(None, stripped_items))
3456
3457
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003458@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003459def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003460 """Uploads the current changelist to codereview.
3461
3462 Can skip dependency patchset uploads for a branch by running:
3463 git config branch.branch_name.skip-deps-uploads True
3464 To unset run:
3465 git config --unset branch.branch_name.skip-deps-uploads
3466 Can also set the above globally by using the --global flag.
3467 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003468 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3469 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003470 parser.add_option('--bypass-watchlists', action='store_true',
3471 dest='bypass_watchlists',
3472 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003473 parser.add_option('-f', action='store_true', dest='force',
3474 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003475 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003476 parser.add_option('-t', dest='title',
3477 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003478 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003479 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003480 help='reviewer email addresses')
3481 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003482 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003483 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003484 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003485 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003486 parser.add_option('--emulate_svn_auto_props',
3487 '--emulate-svn-auto-props',
3488 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003489 dest="emulate_svn_auto_props",
3490 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003491 parser.add_option('-c', '--use-commit-queue', action='store_true',
3492 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003493 parser.add_option('--private', action='store_true',
3494 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003495 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003496 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003497 metavar='TARGET',
3498 help='Apply CL to remote ref TARGET. ' +
3499 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003500 parser.add_option('--squash', action='store_true',
3501 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003502 parser.add_option('--no-squash', action='store_true',
3503 help='Don\'t squash multiple commits into one ' +
3504 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003505 parser.add_option('--email', default=None,
3506 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003507 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3508 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003509 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3510 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003511 help='Send the patchset to do a CQ dry run right after '
3512 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003513 parser.add_option('--dependencies', action='store_true',
3514 help='Uploads CLs of all the local branches that depend on '
3515 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003516
rmistry@google.com2dd99862015-06-22 12:22:18 +00003517 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003518 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003519 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003520 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003521 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003522 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003523 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003524
sbc@chromium.org71437c02015-04-09 19:29:40 +00003525 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003526 return 1
3527
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003528 options.reviewers = cleanup_list(options.reviewers)
3529 options.cc = cleanup_list(options.cc)
3530
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003531 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3532 settings.GetIsGerrit()
3533
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003534 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00003535 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003536
3537
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003538def IsSubmoduleMergeCommit(ref):
3539 # When submodules are added to the repo, we expect there to be a single
3540 # non-git-svn merge commit at remote HEAD with a signature comment.
3541 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003542 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003543 return RunGit(cmd) != ''
3544
3545
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003546def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003547 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003548
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003549 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3550 upstream and closes the issue automatically and atomically.
3551
3552 Otherwise (in case of Rietveld):
3553 Squashes branch into a single commit.
3554 Updates changelog with metadata (e.g. pointer to review).
3555 Pushes/dcommits the code upstream.
3556 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003557 """
3558 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3559 help='bypass upload presubmit hook')
3560 parser.add_option('-m', dest='message',
3561 help="override review description")
3562 parser.add_option('-f', action='store_true', dest='force',
3563 help="force yes to questions (don't prompt)")
3564 parser.add_option('-c', dest='contributor',
3565 help="external contributor for patch (appended to " +
3566 "description and used as author for git). Should be " +
3567 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003568 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003569 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003570 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003571 auth_config = auth.extract_auth_config_from_options(options)
3572
3573 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003574
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003575 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3576 if cl.IsGerrit():
3577 if options.message:
3578 # This could be implemented, but it requires sending a new patch to
3579 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3580 # Besides, Gerrit has the ability to change the commit message on submit
3581 # automatically, thus there is no need to support this option (so far?).
3582 parser.error('-m MESSAGE option is not supported for Gerrit.')
3583 if options.contributor:
3584 parser.error(
3585 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3586 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3587 'the contributor\'s "name <email>". If you can\'t upload such a '
3588 'commit for review, contact your repository admin and request'
3589 '"Forge-Author" permission.')
3590 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3591 options.verbose)
3592
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003593 current = cl.GetBranch()
3594 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3595 if not settings.GetIsGitSvn() and remote == '.':
3596 print
3597 print 'Attempting to push branch %r into another local branch!' % current
3598 print
3599 print 'Either reparent this branch on top of origin/master:'
3600 print ' git reparent-branch --root'
3601 print
3602 print 'OR run `git rebase-update` if you think the parent branch is already'
3603 print 'committed.'
3604 print
3605 print ' Current parent: %r' % upstream_branch
3606 return 1
3607
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003608 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003609 # Default to merging against our best guess of the upstream branch.
3610 args = [cl.GetUpstreamBranch()]
3611
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003612 if options.contributor:
3613 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
3614 print "Please provide contibutor as 'First Last <email@example.com>'"
3615 return 1
3616
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003617 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003618 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003619
sbc@chromium.org71437c02015-04-09 19:29:40 +00003620 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003621 return 1
3622
3623 # This rev-list syntax means "show all commits not in my branch that
3624 # are in base_branch".
3625 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3626 base_branch]).splitlines()
3627 if upstream_commits:
3628 print ('Base branch "%s" has %d commits '
3629 'not in this branch.' % (base_branch, len(upstream_commits)))
3630 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
3631 return 1
3632
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003633 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003634 svn_head = None
3635 if cmd == 'dcommit' or base_has_submodules:
3636 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
3637 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003638
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003639 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003640 # If the base_head is a submodule merge commit, the first parent of the
3641 # base_head should be a git-svn commit, which is what we're interested in.
3642 base_svn_head = base_branch
3643 if base_has_submodules:
3644 base_svn_head += '^1'
3645
3646 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003647 if extra_commits:
3648 print ('This branch has %d additional commits not upstreamed yet.'
3649 % len(extra_commits.splitlines()))
3650 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
3651 'before attempting to %s.' % (base_branch, cmd))
3652 return 1
3653
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003654 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003655 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003656 author = None
3657 if options.contributor:
3658 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003659 hook_results = cl.RunHook(
3660 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003661 may_prompt=not options.force,
3662 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003663 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003664 if not hook_results.should_continue():
3665 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003666
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003667 # Check the tree status if the tree status URL is set.
3668 status = GetTreeStatus()
3669 if 'closed' == status:
3670 print('The tree is closed. Please wait for it to reopen. Use '
3671 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3672 return 1
3673 elif 'unknown' == status:
3674 print('Unable to determine tree status. Please verify manually and '
3675 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3676 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003677
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003678 change_desc = ChangeDescription(options.message)
3679 if not change_desc.description and cl.GetIssue():
3680 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003681
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003682 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00003683 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003684 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00003685 else:
3686 print 'No description set.'
3687 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
3688 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003689
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003690 # Keep a separate copy for the commit message, because the commit message
3691 # contains the link to the Rietveld issue, while the Rietveld message contains
3692 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003693 # Keep a separate copy for the commit message.
3694 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00003695 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003696
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003697 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00003698 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00003699 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00003700 # after it. Add a period on a new line to circumvent this. Also add a space
3701 # before the period to make sure that Gitiles continues to correctly resolve
3702 # the URL.
3703 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003704 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003705 commit_desc.append_footer('Patch from %s.' % options.contributor)
3706
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00003707 print('Description:')
3708 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003709
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003710 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003711 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00003712 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003713
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003714 # We want to squash all this branch's commits into one commit with the proper
3715 # description. We do this by doing a "reset --soft" to the base branch (which
3716 # keeps the working copy the same), then dcommitting that. If origin/master
3717 # has a submodule merge commit, we'll also need to cherry-pick the squashed
3718 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003719 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003720 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
3721 # Delete the branches if they exist.
3722 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
3723 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
3724 result = RunGitWithCode(showref_cmd)
3725 if result[0] == 0:
3726 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003727
3728 # We might be in a directory that's present in this branch but not in the
3729 # trunk. Move up to the top of the tree so that git commands that expect a
3730 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003731 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003732 if rel_base_path:
3733 os.chdir(rel_base_path)
3734
3735 # Stuff our change into the merge branch.
3736 # We wrap in a try...finally block so if anything goes wrong,
3737 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003738 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003739 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003740 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003741 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003742 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00003743 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003744 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003745 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003746 RunGit(
3747 [
3748 'commit', '--author', options.contributor,
3749 '-m', commit_desc.description,
3750 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003751 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003752 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003753 if base_has_submodules:
3754 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
3755 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
3756 RunGit(['checkout', CHERRY_PICK_BRANCH])
3757 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003758 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003759 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003760 mirror = settings.GetGitMirror(remote)
3761 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003762 pending_prefix = settings.GetPendingRefPrefix()
3763 if not pending_prefix or branch.startswith(pending_prefix):
3764 # If not using refs/pending/heads/* at all, or target ref is already set
3765 # to pending, then push to the target ref directly.
3766 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003767 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003768 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003769 else:
3770 # Cherry-pick the change on top of pending ref and then push it.
3771 assert branch.startswith('refs/'), branch
3772 assert pending_prefix[-1] == '/', pending_prefix
3773 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003774 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003775 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003776 if retcode == 0:
3777 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003778 else:
3779 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003780 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003781 'svn', 'dcommit',
3782 '-C%s' % options.similarity,
3783 '--no-rebase', '--rmdir',
3784 ]
3785 if settings.GetForceHttpsCommitUrl():
3786 # Allow forcing https commit URLs for some projects that don't allow
3787 # committing to http URLs (like Google Code).
3788 remote_url = cl.GetGitSvnRemoteUrl()
3789 if urlparse.urlparse(remote_url).scheme == 'http':
3790 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003791 cmd_args.append('--commit-url=%s' % remote_url)
3792 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003793 if 'Committed r' in output:
3794 revision = re.match(
3795 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
3796 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003797 finally:
3798 # And then swap back to the original branch and clean up.
3799 RunGit(['checkout', '-q', cl.GetBranch()])
3800 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003801 if base_has_submodules:
3802 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003803
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003804 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003805 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003806 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003807
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003808 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003809 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003810 try:
3811 revision = WaitForRealCommit(remote, revision, base_branch, branch)
3812 # We set pushed_to_pending to False, since it made it all the way to the
3813 # real ref.
3814 pushed_to_pending = False
3815 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003816 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003817
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003818 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003819 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003820 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003821 if not to_pending:
3822 if viewvc_url and revision:
3823 change_desc.append_footer(
3824 'Committed: %s%s' % (viewvc_url, revision))
3825 elif revision:
3826 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003827 print ('Closing issue '
3828 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003829 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003830 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003831 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00003832 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00003833 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00003834 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003835 if options.bypass_hooks:
3836 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
3837 else:
3838 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00003839 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003840 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003841
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003842 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003843 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
3844 print 'The commit is in the pending queue (%s).' % pending_ref
3845 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00003846 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003847 'footer.' % branch)
3848
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003849 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
3850 if os.path.isfile(hook):
3851 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003852
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003853 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003854
3855
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003856def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
3857 print
3858 print 'Waiting for commit to be landed on %s...' % real_ref
3859 print '(If you are impatient, you may Ctrl-C once without harm)'
3860 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
3861 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003862 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003863
3864 loop = 0
3865 while True:
3866 sys.stdout.write('fetching (%d)... \r' % loop)
3867 sys.stdout.flush()
3868 loop += 1
3869
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003870 if mirror:
3871 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003872 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
3873 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
3874 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
3875 for commit in commits.splitlines():
3876 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
3877 print 'Found commit on %s' % real_ref
3878 return commit
3879
3880 current_rev = to_rev
3881
3882
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003883def PushToGitPending(remote, pending_ref, upstream_ref):
3884 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
3885
3886 Returns:
3887 (retcode of last operation, output log of last operation).
3888 """
3889 assert pending_ref.startswith('refs/'), pending_ref
3890 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
3891 cherry = RunGit(['rev-parse', 'HEAD']).strip()
3892 code = 0
3893 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003894 max_attempts = 3
3895 attempts_left = max_attempts
3896 while attempts_left:
3897 if attempts_left != max_attempts:
3898 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
3899 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003900
3901 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003902 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003903 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003904 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003905 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003906 print 'Fetch failed with exit code %d.' % code
3907 if out.strip():
3908 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003909 continue
3910
3911 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003912 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003913 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003914 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003915 if code:
3916 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003917 'Your patch doesn\'t apply cleanly to ref \'%s\', '
3918 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003919 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
3920 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003921 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003922 return code, out
3923
3924 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003925 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003926 code, out = RunGitWithCode(
3927 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
3928 if code == 0:
3929 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003930 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003931 return code, out
3932
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003933 print 'Push failed with exit code %d.' % code
3934 if out.strip():
3935 print out.strip()
3936 if IsFatalPushFailure(out):
3937 print (
3938 'Fatal push error. Make sure your .netrc credentials and git '
3939 'user.email are correct and you have push access to the repo.')
3940 return code, out
3941
3942 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003943 return code, out
3944
3945
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003946def IsFatalPushFailure(push_stdout):
3947 """True if retrying push won't help."""
3948 return '(prohibited by Gerrit)' in push_stdout
3949
3950
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003951@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003952def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003953 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003954 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003955 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003956 # If it looks like previous commits were mirrored with git-svn.
3957 message = """This repository appears to be a git-svn mirror, but no
3958upstream SVN master is set. You probably need to run 'git auto-svn' once."""
3959 else:
3960 message = """This doesn't appear to be an SVN repository.
3961If your project has a true, writeable git repository, you probably want to run
3962'git cl land' instead.
3963If your project has a git mirror of an upstream SVN master, you probably need
3964to run 'git svn init'.
3965
3966Using the wrong command might cause your commit to appear to succeed, and the
3967review to be closed, without actually landing upstream. If you choose to
3968proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00003969 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00003970 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003971 return SendUpstream(parser, args, 'dcommit')
3972
3973
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003974@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003975def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003976 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003977 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003978 print('This appears to be an SVN repository.')
3979 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003980 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00003981 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003982 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003983
3984
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003985@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003986def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00003987 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003988 parser.add_option('-b', dest='newbranch',
3989 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003990 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003991 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003992 parser.add_option('-d', '--directory', action='store', metavar='DIR',
3993 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003994 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003995 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003996 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003997 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003998 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003999 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004000
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004001
4002 group = optparse.OptionGroup(
4003 parser,
4004 'Options for continuing work on the current issue uploaded from a '
4005 'different clone (e.g. different machine). Must be used independently '
4006 'from the other options. No issue number should be specified, and the '
4007 'branch must have an issue number associated with it')
4008 group.add_option('--reapply', action='store_true', dest='reapply',
4009 help='Reset the branch and reapply the issue.\n'
4010 'CAUTION: This will undo any local changes in this '
4011 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004012
4013 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004014 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004015 parser.add_option_group(group)
4016
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004017 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004018 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004019 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004020 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004021 auth_config = auth.extract_auth_config_from_options(options)
4022
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004023 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004024
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004025 issue_arg = None
4026 if options.reapply :
4027 if len(args) > 0:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004028 parser.error('--reapply implies no additional arguments.')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004029
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004030 issue_arg = cl.GetIssue()
4031 upstream = cl.GetUpstreamBranch()
4032 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004033 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004034
4035 RunGit(['reset', '--hard', upstream])
4036 if options.pull:
4037 RunGit(['pull'])
4038 else:
4039 if len(args) != 1:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004040 parser.error('Must specify issue number or url')
4041 issue_arg = args[0]
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004042
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004043 if not issue_arg:
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004044 parser.print_help()
4045 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004046
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004047 if cl.IsGerrit():
4048 if options.reject:
4049 parser.error('--reject is not supported with Gerrit codereview.')
4050 if options.nocommit:
4051 parser.error('--nocommit is not supported with Gerrit codereview.')
4052 if options.directory:
4053 parser.error('--directory is not supported with Gerrit codereview.')
4054
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004055 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004056 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004057 return 1
4058
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004059 if options.newbranch:
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004060 if options.reapply:
4061 parser.error("--reapply excludes any option other than --pull")
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004062 if options.force:
4063 RunGit(['branch', '-D', options.newbranch],
4064 stderr=subprocess2.PIPE, error_ok=True)
4065 RunGit(['checkout', '-b', options.newbranch,
4066 Changelist().GetUpstreamBranch()])
4067
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004068 return cl.CMDPatchIssue(issue_arg, options.reject, options.nocommit,
4069 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004070
4071
4072def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004073 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004074 # Provide a wrapper for git svn rebase to help avoid accidental
4075 # git svn dcommit.
4076 # It's the only command that doesn't use parser at all since we just defer
4077 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004078
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004079 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004080
4081
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004082def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004083 """Fetches the tree status and returns either 'open', 'closed',
4084 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004085 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004086 if url:
4087 status = urllib2.urlopen(url).read().lower()
4088 if status.find('closed') != -1 or status == '0':
4089 return 'closed'
4090 elif status.find('open') != -1 or status == '1':
4091 return 'open'
4092 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004093 return 'unset'
4094
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004095
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004096def GetTreeStatusReason():
4097 """Fetches the tree status from a json url and returns the message
4098 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004099 url = settings.GetTreeStatusUrl()
4100 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004101 connection = urllib2.urlopen(json_url)
4102 status = json.loads(connection.read())
4103 connection.close()
4104 return status['message']
4105
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004106
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004107def GetBuilderMaster(bot_list):
4108 """For a given builder, fetch the master from AE if available."""
4109 map_url = 'https://builders-map.appspot.com/'
4110 try:
4111 master_map = json.load(urllib2.urlopen(map_url))
4112 except urllib2.URLError as e:
4113 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4114 (map_url, e))
4115 except ValueError as e:
4116 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4117 if not master_map:
4118 return None, 'Failed to build master map.'
4119
4120 result_master = ''
4121 for bot in bot_list:
4122 builder = bot.split(':', 1)[0]
4123 master_list = master_map.get(builder, [])
4124 if not master_list:
4125 return None, ('No matching master for builder %s.' % builder)
4126 elif len(master_list) > 1:
4127 return None, ('The builder name %s exists in multiple masters %s.' %
4128 (builder, master_list))
4129 else:
4130 cur_master = master_list[0]
4131 if not result_master:
4132 result_master = cur_master
4133 elif result_master != cur_master:
4134 return None, 'The builders do not belong to the same master.'
4135 return result_master, None
4136
4137
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004138def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004139 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004140 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004141 status = GetTreeStatus()
4142 if 'unset' == status:
4143 print 'You must configure your tree status URL by running "git cl config".'
4144 return 2
4145
4146 print "The tree is %s" % status
4147 print
4148 print GetTreeStatusReason()
4149 if status != 'open':
4150 return 1
4151 return 0
4152
4153
maruel@chromium.org15192402012-09-06 12:38:29 +00004154def CMDtry(parser, args):
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004155 """Triggers try jobs through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00004156 group = optparse.OptionGroup(parser, "Try job options")
4157 group.add_option(
4158 "-b", "--bot", action="append",
4159 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
4160 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004161 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00004162 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004163 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00004164 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004165 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00004166 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004167 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004168 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004169 "-r", "--revision",
4170 help="Revision to use for the try job; default: the "
4171 "revision will be determined by the try server; see "
4172 "its waterfall for more info")
4173 group.add_option(
4174 "-c", "--clobber", action="store_true", default=False,
4175 help="Force a clobber before building; e.g. don't do an "
4176 "incremental build")
4177 group.add_option(
4178 "--project",
4179 help="Override which project to use. Projects are defined "
4180 "server-side to define what default bot set to use")
4181 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004182 "-p", "--property", dest="properties", action="append", default=[],
4183 help="Specify generic properties in the form -p key1=value1 -p "
4184 "key2=value2 etc (buildbucket only). The value will be treated as "
4185 "json if decodable, or as string otherwise.")
4186 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004187 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004188 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004189 "--use-rietveld", action="store_true", default=False,
4190 help="Use Rietveld to trigger try jobs.")
4191 group.add_option(
4192 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4193 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004194 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004195 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004196 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004197 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004198
machenbach@chromium.org45453142015-09-15 08:45:22 +00004199 if options.use_rietveld and options.properties:
4200 parser.error('Properties can only be specified with buildbucket')
4201
4202 # Make sure that all properties are prop=value pairs.
4203 bad_params = [x for x in options.properties if '=' not in x]
4204 if bad_params:
4205 parser.error('Got properties with missing "=": %s' % bad_params)
4206
maruel@chromium.org15192402012-09-06 12:38:29 +00004207 if args:
4208 parser.error('Unknown arguments: %s' % args)
4209
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004210 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004211 if not cl.GetIssue():
4212 parser.error('Need to upload first')
4213
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004214 if cl.IsGerrit():
4215 parser.error(
4216 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4217 'If your project has Commit Queue, dry run is a workaround:\n'
4218 ' git cl set-commit --dry-run')
4219 # Code below assumes Rietveld issue.
4220 # TODO(tandrii): actually implement for Gerrit http://crbug.com/599931.
4221
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004222 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004223 if props.get('closed'):
4224 parser.error('Cannot send tryjobs for a closed CL')
4225
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004226 if props.get('private'):
4227 parser.error('Cannot use trybots with private issue')
4228
maruel@chromium.org15192402012-09-06 12:38:29 +00004229 if not options.name:
4230 options.name = cl.GetBranch()
4231
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004232 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004233 options.master, err_msg = GetBuilderMaster(options.bot)
4234 if err_msg:
4235 parser.error('Tryserver master cannot be found because: %s\n'
4236 'Please manually specify the tryserver master'
4237 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004238
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004239 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004240 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004241 if not options.bot:
4242 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004243
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004244 # Get try masters from PRESUBMIT.py files.
4245 masters = presubmit_support.DoGetTryMasters(
4246 change,
4247 change.LocalPaths(),
4248 settings.GetRoot(),
4249 None,
4250 None,
4251 options.verbose,
4252 sys.stdout)
4253 if masters:
4254 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004255
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004256 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4257 options.bot = presubmit_support.DoGetTrySlaves(
4258 change,
4259 change.LocalPaths(),
4260 settings.GetRoot(),
4261 None,
4262 None,
4263 options.verbose,
4264 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004265
4266 if not options.bot:
4267 # Get try masters from cq.cfg if any.
4268 # TODO(tandrii): some (but very few) projects store cq.cfg in different
4269 # location.
4270 cq_cfg = os.path.join(change.RepositoryRoot(),
4271 'infra', 'config', 'cq.cfg')
4272 if os.path.exists(cq_cfg):
4273 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00004274 cq_masters = commit_queue.get_master_builder_map(
4275 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004276 for master, builders in cq_masters.iteritems():
4277 for builder in builders:
4278 # Skip presubmit builders, because these will fail without LGTM.
4279 if 'presubmit' not in builder.lower():
4280 masters.setdefault(master, {})[builder] = ['defaulttests']
4281 if masters:
4282 return masters
4283
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004284 if not options.bot:
4285 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00004286
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004287 builders_and_tests = {}
4288 # TODO(machenbach): The old style command-line options don't support
4289 # multiple try masters yet.
4290 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4291 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4292
4293 for bot in old_style:
4294 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004295 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004296 elif ',' in bot:
4297 parser.error('Specify one bot per --bot flag')
4298 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004299 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004300
4301 for bot, tests in new_style:
4302 builders_and_tests.setdefault(bot, []).extend(tests)
4303
4304 # Return a master map with one master to be backwards compatible. The
4305 # master name defaults to an empty string, which will cause the master
4306 # not to be set on rietveld (deprecated).
4307 return {options.master: builders_and_tests}
4308
4309 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004310
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004311 for builders in masters.itervalues():
4312 if any('triggered' in b for b in builders):
4313 print >> sys.stderr, (
4314 'ERROR You are trying to send a job to a triggered bot. This type of'
4315 ' bot requires an\ninitial job from a parent (usually a builder). '
4316 'Instead send your job to the parent.\n'
4317 'Bot list: %s' % builders)
4318 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004319
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004320 patchset = cl.GetMostRecentPatchset()
4321 if patchset and patchset != cl.GetPatchset():
4322 print(
4323 '\nWARNING Mismatch between local config and server. Did a previous '
4324 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4325 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004326 if options.luci:
4327 trigger_luci_job(cl, masters, options)
4328 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004329 try:
4330 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4331 except BuildbucketResponseException as ex:
4332 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004333 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004334 except Exception as e:
4335 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4336 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
4337 e, stacktrace)
4338 return 1
4339 else:
4340 try:
4341 cl.RpcServer().trigger_distributed_try_jobs(
4342 cl.GetIssue(), patchset, options.name, options.clobber,
4343 options.revision, masters)
4344 except urllib2.HTTPError as e:
4345 if e.code == 404:
4346 print('404 from rietveld; '
4347 'did you mean to use "git try" instead of "git cl try"?')
4348 return 1
4349 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004350
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004351 for (master, builders) in sorted(masters.iteritems()):
4352 if master:
4353 print 'Master: %s' % master
4354 length = max(len(builder) for builder in builders)
4355 for builder in sorted(builders):
4356 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00004357 return 0
4358
4359
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004360def CMDtry_results(parser, args):
4361 group = optparse.OptionGroup(parser, "Try job results options")
4362 group.add_option(
4363 "-p", "--patchset", type=int, help="patchset number if not current.")
4364 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004365 "--print-master", action='store_true', help="print master name as well.")
4366 group.add_option(
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004367 "--color", action='store_true', default=setup_color.IS_TTY,
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004368 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004369 group.add_option(
4370 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4371 help="Host of buildbucket. The default host is %default.")
4372 parser.add_option_group(group)
4373 auth.add_auth_options(parser)
4374 options, args = parser.parse_args(args)
4375 if args:
4376 parser.error('Unrecognized args: %s' % ' '.join(args))
4377
4378 auth_config = auth.extract_auth_config_from_options(options)
4379 cl = Changelist(auth_config=auth_config)
4380 if not cl.GetIssue():
4381 parser.error('Need to upload first')
4382
4383 if not options.patchset:
4384 options.patchset = cl.GetMostRecentPatchset()
4385 if options.patchset and options.patchset != cl.GetPatchset():
4386 print(
4387 '\nWARNING Mismatch between local config and server. Did a previous '
4388 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4389 'Continuing using\npatchset %s.\n' % options.patchset)
4390 try:
4391 jobs = fetch_try_jobs(auth_config, cl, options)
4392 except BuildbucketResponseException as ex:
4393 print 'Buildbucket error: %s' % ex
4394 return 1
4395 except Exception as e:
4396 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4397 print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
4398 e, stacktrace)
4399 return 1
4400 print_tryjobs(options, jobs)
4401 return 0
4402
4403
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004404@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004405def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004406 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004407 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004408 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004409 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004410
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004411 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004412 if args:
4413 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004414 branch = cl.GetBranch()
4415 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004416 cl = Changelist()
4417 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004418
4419 # Clear configured merge-base, if there is one.
4420 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004421 else:
4422 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004423 return 0
4424
4425
thestig@chromium.org00858c82013-12-02 23:08:03 +00004426def CMDweb(parser, args):
4427 """Opens the current CL in the web browser."""
4428 _, args = parser.parse_args(args)
4429 if args:
4430 parser.error('Unrecognized args: %s' % ' '.join(args))
4431
4432 issue_url = Changelist().GetIssueURL()
4433 if not issue_url:
4434 print >> sys.stderr, 'ERROR No issue to open'
4435 return 1
4436
4437 webbrowser.open(issue_url)
4438 return 0
4439
4440
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004441def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004442 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004443 parser.add_option('-d', '--dry-run', action='store_true',
4444 help='trigger in dry run mode')
4445 parser.add_option('-c', '--clear', action='store_true',
4446 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004447 auth.add_auth_options(parser)
4448 options, args = parser.parse_args(args)
4449 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004450 if args:
4451 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004452 if options.dry_run and options.clear:
4453 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4454
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004455 cl = Changelist(auth_config=auth_config)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004456 if options.clear:
4457 state = _CQState.CLEAR
4458 elif options.dry_run:
4459 state = _CQState.DRY_RUN
4460 else:
4461 state = _CQState.COMMIT
4462 if not cl.GetIssue():
4463 parser.error('Must upload the issue first')
4464 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004465 return 0
4466
4467
groby@chromium.org411034a2013-02-26 15:12:01 +00004468def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004469 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004470 auth.add_auth_options(parser)
4471 options, args = parser.parse_args(args)
4472 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004473 if args:
4474 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004475 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00004476 # Ensure there actually is an issue to close.
4477 cl.GetDescription()
4478 cl.CloseIssue()
4479 return 0
4480
4481
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004482def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004483 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004484 auth.add_auth_options(parser)
4485 options, args = parser.parse_args(args)
4486 auth_config = auth.extract_auth_config_from_options(options)
4487 if args:
4488 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004489
4490 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004491 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004492 # Staged changes would be committed along with the patch from last
4493 # upload, hence counted toward the "last upload" side in the final
4494 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004495 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004496 return 1
4497
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004498 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004499 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004500 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004501 if not issue:
4502 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004503 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004504 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004505
4506 # Create a new branch based on the merge-base
4507 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004508 # Clear cached branch in cl object, to avoid overwriting original CL branch
4509 # properties.
4510 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004511 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004512 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004513 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004514 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004515 return rtn
4516
wychen@chromium.org06928532015-02-03 02:11:29 +00004517 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004518 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00004519 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004520 finally:
4521 RunGit(['checkout', '-q', branch])
4522 RunGit(['branch', '-D', TMP_BRANCH])
4523
4524 return 0
4525
4526
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004527def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004528 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004529 parser.add_option(
4530 '--no-color',
4531 action='store_true',
4532 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004533 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004534 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004535 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004536
4537 author = RunGit(['config', 'user.email']).strip() or None
4538
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004539 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004540
4541 if args:
4542 if len(args) > 1:
4543 parser.error('Unknown args')
4544 base_branch = args[0]
4545 else:
4546 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004547 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004548
4549 change = cl.GetChange(base_branch, None)
4550 return owners_finder.OwnersFinder(
4551 [f.LocalPath() for f in
4552 cl.GetChange(base_branch, None).AffectedFiles()],
4553 change.RepositoryRoot(), author,
4554 fopen=file, os_path=os.path, glob=glob.glob,
4555 disable_color=options.no_color).run()
4556
4557
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004558def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004559 """Generates a diff command."""
4560 # Generate diff for the current branch's changes.
4561 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4562 upstream_commit, '--' ]
4563
4564 if args:
4565 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004566 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004567 diff_cmd.append(arg)
4568 else:
4569 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004570
4571 return diff_cmd
4572
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004573def MatchingFileType(file_name, extensions):
4574 """Returns true if the file name ends with one of the given extensions."""
4575 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004576
enne@chromium.org555cfe42014-01-29 18:21:39 +00004577@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004578def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004579 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00004580 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004581 GN_EXTS = ['.gn', '.gni']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004582 parser.add_option('--full', action='store_true',
4583 help='Reformat the full content of all touched files')
4584 parser.add_option('--dry-run', action='store_true',
4585 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004586 parser.add_option('--python', action='store_true',
4587 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004588 parser.add_option('--diff', action='store_true',
4589 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004590 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004591
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004592 # git diff generates paths against the root of the repository. Change
4593 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004594 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004595 if rel_base_path:
4596 os.chdir(rel_base_path)
4597
digit@chromium.org29e47272013-05-17 17:01:46 +00004598 # Grab the merge-base commit, i.e. the upstream commit of the current
4599 # branch when it was created or the last time it was rebased. This is
4600 # to cover the case where the user may have called "git fetch origin",
4601 # moving the origin branch to a newer commit, but hasn't rebased yet.
4602 upstream_commit = None
4603 cl = Changelist()
4604 upstream_branch = cl.GetUpstreamBranch()
4605 if upstream_branch:
4606 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4607 upstream_commit = upstream_commit.strip()
4608
4609 if not upstream_commit:
4610 DieWithError('Could not find base commit for this branch. '
4611 'Are you in detached state?')
4612
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004613 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4614 diff_output = RunGit(changed_files_cmd)
4615 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004616 # Filter out files deleted by this CL
4617 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004618
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004619 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4620 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4621 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004622 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004623
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004624 top_dir = os.path.normpath(
4625 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
4626
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004627 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4628 # formatted. This is used to block during the presubmit.
4629 return_value = 0
4630
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004631 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004632 # Locate the clang-format binary in the checkout
4633 try:
4634 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
4635 except clang_format.NotFoundError, e:
4636 DieWithError(e)
4637
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004638 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004639 cmd = [clang_format_tool]
4640 if not opts.dry_run and not opts.diff:
4641 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004642 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004643 if opts.diff:
4644 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004645 else:
4646 env = os.environ.copy()
4647 env['PATH'] = str(os.path.dirname(clang_format_tool))
4648 try:
4649 script = clang_format.FindClangFormatScriptInChromiumTree(
4650 'clang-format-diff.py')
4651 except clang_format.NotFoundError, e:
4652 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004653
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004654 cmd = [sys.executable, script, '-p0']
4655 if not opts.dry_run and not opts.diff:
4656 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004657
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004658 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
4659 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004660
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004661 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
4662 if opts.diff:
4663 sys.stdout.write(stdout)
4664 if opts.dry_run and len(stdout) > 0:
4665 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004666
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004667 # Similar code to above, but using yapf on .py files rather than clang-format
4668 # on C/C++ files
4669 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004670 yapf_tool = gclient_utils.FindExecutable('yapf')
4671 if yapf_tool is None:
4672 DieWithError('yapf not found in PATH')
4673
4674 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004675 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004676 cmd = [yapf_tool]
4677 if not opts.dry_run and not opts.diff:
4678 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004679 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004680 if opts.diff:
4681 sys.stdout.write(stdout)
4682 else:
4683 # TODO(sbc): yapf --lines mode still has some issues.
4684 # https://github.com/google/yapf/issues/154
4685 DieWithError('--python currently only works with --full')
4686
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004687 # Dart's formatter does not have the nice property of only operating on
4688 # modified chunks, so hard code full.
4689 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004690 try:
4691 command = [dart_format.FindDartFmtToolInChromiumTree()]
4692 if not opts.dry_run and not opts.diff:
4693 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004694 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004695
ppi@chromium.org6593d932016-03-03 15:41:15 +00004696 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004697 if opts.dry_run and stdout:
4698 return_value = 2
4699 except dart_format.NotFoundError as e:
erikcorry@chromium.org3e445022015-12-17 09:07:26 +00004700 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
4701 'found in this checkout. Files in other languages are still ' +
4702 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004703
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004704 # Format GN build files. Always run on full build files for canonical form.
4705 if gn_diff_files:
4706 cmd = ['gn', 'format']
4707 if not opts.dry_run and not opts.diff:
4708 cmd.append('--in-place')
4709 for gn_diff_file in gn_diff_files:
4710 stdout = RunCommand(cmd + [gn_diff_file], cwd=top_dir)
4711 if opts.diff:
4712 sys.stdout.write(stdout)
4713
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004714 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004715
4716
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004717@subcommand.usage('<codereview url or issue id>')
4718def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004719 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004720 _, args = parser.parse_args(args)
4721
4722 if len(args) != 1:
4723 parser.print_help()
4724 return 1
4725
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004726 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00004727 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004728 parser.print_help()
4729 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00004730 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004731
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004732 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00004733 output = RunGit(['config', '--local', '--get-regexp',
4734 r'branch\..*\.%s' % issueprefix],
4735 error_ok=True)
4736 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004737 if issue == target_issue:
4738 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004739
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004740 branches = []
4741 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00004742 branches.extend(find_issues(cls.IssueSettingSuffix()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004743 if len(branches) == 0:
4744 print 'No branch found for issue %s.' % target_issue
4745 return 1
4746 if len(branches) == 1:
4747 RunGit(['checkout', branches[0]])
4748 else:
4749 print 'Multiple branches match issue %s:' % target_issue
4750 for i in range(len(branches)):
4751 print '%d: %s' % (i, branches[i])
4752 which = raw_input('Choose by index: ')
4753 try:
4754 RunGit(['checkout', branches[int(which)]])
4755 except (IndexError, ValueError):
4756 print 'Invalid selection, not checking out any branch.'
4757 return 1
4758
4759 return 0
4760
4761
maruel@chromium.org29404b52014-09-08 22:58:00 +00004762def CMDlol(parser, args):
4763 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00004764 print zlib.decompress(base64.b64decode(
4765 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
4766 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
4767 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
4768 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00004769 return 0
4770
4771
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004772class OptionParser(optparse.OptionParser):
4773 """Creates the option parse and add --verbose support."""
4774 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004775 optparse.OptionParser.__init__(
4776 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004777 self.add_option(
4778 '-v', '--verbose', action='count', default=0,
4779 help='Use 2 times for more debugging info')
4780
4781 def parse_args(self, args=None, values=None):
4782 options, args = optparse.OptionParser.parse_args(self, args, values)
4783 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
4784 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
4785 return options, args
4786
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004787
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004788def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00004789 if sys.hexversion < 0x02060000:
4790 print >> sys.stderr, (
4791 '\nYour python version %s is unsupported, please upgrade.\n' %
4792 sys.version.split(' ', 1)[0])
4793 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004794
maruel@chromium.orgddd59412011-11-30 14:20:38 +00004795 # Reload settings.
4796 global settings
4797 settings = Settings()
4798
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004799 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004800 dispatcher = subcommand.CommandDispatcher(__name__)
4801 try:
4802 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00004803 except auth.AuthenticationError as e:
4804 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004805 except urllib2.HTTPError, e:
4806 if e.code != 500:
4807 raise
4808 DieWithError(
4809 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
4810 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00004811 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004812
4813
4814if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004815 # These affect sys.stdout so do it outside of main() to simplify mocks in
4816 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00004817 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004818 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00004819 try:
4820 sys.exit(main(sys.argv[1:]))
4821 except KeyboardInterrupt:
4822 sys.stderr.write('interrupted\n')
4823 sys.exit(1)