blob: 9bc7d52e753afacf9f65851cad597e78e03e5bca [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
maruel@chromium.org1033efd2013-07-23 23:25:09 +000020import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000022import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000023import sys
bauerb@chromium.org27386dd2015-02-16 10:45:39 +000024import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025import textwrap
sheyang@google.com6ebaf782015-05-12 19:17:54 +000026import time
27import traceback
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000028import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000030import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000031import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000032import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000033import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000034
35try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000036 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000037except ImportError:
38 pass
39
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000040from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000041from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000043import auth
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +000044from luci_hacks import trigger_luci_job as luci_trigger
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000045import clang_format
tandrii@chromium.org71184c02016-01-13 15:18:44 +000046import commit_queue
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000047import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000048import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000049import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000050import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000051import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000052import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000053import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000054import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000055import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000056import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000057import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000058import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000059import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000060import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000061import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000062import watchlists
63
maruel@chromium.org0633fb42013-08-16 20:06:14 +000064__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000065
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000066DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000067POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000069GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
rmistry@google.comc68112d2015-03-03 12:48:06 +000070REFS_THAT_ALIAS_TO_OTHER_REFS = {
71 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
72 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
73}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000074
thestig@chromium.org44202a22014-03-11 19:22:18 +000075# Valid extensions for files we want to lint.
76DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
77DEFAULT_LINT_IGNORE_REGEX = r"$^"
78
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000079# Shortcut since it quickly becomes redundant.
80Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000081
maruel@chromium.orgddd59412011-11-30 14:20:38 +000082# Initialized in main()
83settings = None
84
85
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000086def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000087 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000088 sys.exit(1)
89
90
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000091def GetNoGitPagerEnv():
92 env = os.environ.copy()
93 # 'cat' is a magical git string that disables pagers on all platforms.
94 env['GIT_PAGER'] = 'cat'
95 return env
96
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000097
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000098def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000099 try:
maruel@chromium.org373af802012-05-25 21:07:33 +0000100 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000101 except subprocess2.CalledProcessError as e:
102 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000103 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000104 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000105 'Command "%s" failed.\n%s' % (
106 ' '.join(args), error_message or e.stdout or ''))
107 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000108
109
110def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000111 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000112 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000113
114
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000115def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000116 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000117 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000118 if suppress_stderr:
119 stderr = subprocess2.VOID
120 else:
121 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000122 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000123 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000124 stdout=subprocess2.PIPE,
125 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000126 return code, out[0]
127 except ValueError:
128 # When the subprocess fails, it returns None. That triggers a ValueError
129 # when trying to unpack the return value into (out, code).
130 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000131
132
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000133def RunGitSilent(args):
134 """Returns stdout, suppresses stderr and ingores the return code."""
135 return RunGitWithCode(args, suppress_stderr=True)[1]
136
137
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000138def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000139 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000140 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000141 return (version.startswith(prefix) and
142 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000143
144
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000145def BranchExists(branch):
146 """Return True if specified branch exists."""
147 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
148 suppress_stderr=True)
149 return not code
150
151
maruel@chromium.org90541732011-04-01 17:54:18 +0000152def ask_for_data(prompt):
153 try:
154 return raw_input(prompt)
155 except KeyboardInterrupt:
156 # Hide the exception.
157 sys.exit(1)
158
159
iannucci@chromium.org79540052012-10-19 23:15:26 +0000160def git_set_branch_value(key, value):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000161 branch = GetCurrentBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000162 if not branch:
163 return
164
165 cmd = ['config']
166 if isinstance(value, int):
167 cmd.append('--int')
168 git_key = 'branch.%s.%s' % (branch, key)
169 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000170
171
172def git_get_branch_default(key, default):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000173 branch = GetCurrentBranch()
iannucci@chromium.org79540052012-10-19 23:15:26 +0000174 if branch:
175 git_key = 'branch.%s.%s' % (branch, key)
176 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
177 try:
178 return int(stdout.strip())
179 except ValueError:
180 pass
181 return default
182
183
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000184def add_git_similarity(parser):
185 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000186 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000187 help='Sets the percentage that a pair of files need to match in order to'
188 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000189 parser.add_option(
190 '--find-copies', action='store_true',
191 help='Allows git to look for copies.')
192 parser.add_option(
193 '--no-find-copies', action='store_false', dest='find_copies',
194 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000195
196 old_parser_args = parser.parse_args
197 def Parse(args):
198 options, args = old_parser_args(args)
199
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000200 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000201 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000202 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000203 print('Note: Saving similarity of %d%% in git config.'
204 % options.similarity)
205 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000206
iannucci@chromium.org79540052012-10-19 23:15:26 +0000207 options.similarity = max(0, min(options.similarity, 100))
208
209 if options.find_copies is None:
210 options.find_copies = bool(
211 git_get_branch_default('git-find-copies', True))
212 else:
213 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000214
215 print('Using %d%% similarity for rename/copy detection. '
216 'Override with --similarity.' % options.similarity)
217
218 return options, args
219 parser.parse_args = Parse
220
221
machenbach@chromium.org45453142015-09-15 08:45:22 +0000222def _get_properties_from_options(options):
223 properties = dict(x.split('=', 1) for x in options.properties)
224 for key, val in properties.iteritems():
225 try:
226 properties[key] = json.loads(val)
227 except ValueError:
228 pass # If a value couldn't be evaluated, treat it as a string.
229 return properties
230
231
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000232def _prefix_master(master):
233 """Convert user-specified master name to full master name.
234
235 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
236 name, while the developers always use shortened master name
237 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
238 function does the conversion for buildbucket migration.
239 """
240 prefix = 'master.'
241 if master.startswith(prefix):
242 return master
243 return '%s%s' % (prefix, master)
244
245
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000246def _buildbucket_retry(operation_name, http, *args, **kwargs):
247 """Retries requests to buildbucket service and returns parsed json content."""
248 try_count = 0
249 while True:
250 response, content = http.request(*args, **kwargs)
251 try:
252 content_json = json.loads(content)
253 except ValueError:
254 content_json = None
255
256 # Buildbucket could return an error even if status==200.
257 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000258 error = content_json.get('error')
259 if error.get('code') == 403:
260 raise BuildbucketResponseException(
261 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000262 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000263 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000264 raise BuildbucketResponseException(msg)
265
266 if response.status == 200:
267 if not content_json:
268 raise BuildbucketResponseException(
269 'Buildbucket returns invalid json content: %s.\n'
270 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
271 content)
272 return content_json
273 if response.status < 500 or try_count >= 2:
274 raise httplib2.HttpLib2Error(content)
275
276 # status >= 500 means transient failures.
277 logging.debug('Transient errors when %s. Will retry.', operation_name)
278 time.sleep(0.5 + 1.5*try_count)
279 try_count += 1
280 assert False, 'unreachable'
281
282
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000283def trigger_luci_job(changelist, masters, options):
284 """Send a job to run on LUCI."""
285 issue_props = changelist.GetIssueProperties()
286 issue = changelist.GetIssue()
287 patchset = changelist.GetMostRecentPatchset()
288 for builders_and_tests in sorted(masters.itervalues()):
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000289 # TODO(hinoka et al): add support for other properties.
290 # Currently, this completely ignores testfilter and other properties.
291 for builder in sorted(builders_and_tests):
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000292 luci_trigger.trigger(
293 builder, 'HEAD', issue, patchset, issue_props['project'])
294
295
machenbach@chromium.org45453142015-09-15 08:45:22 +0000296def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000297 rietveld_url = settings.GetDefaultServerUrl()
298 rietveld_host = urlparse.urlparse(rietveld_url).hostname
299 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
300 http = authenticator.authorize(httplib2.Http())
301 http.force_exception_to_status_code = True
302 issue_props = changelist.GetIssueProperties()
303 issue = changelist.GetIssue()
304 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000305 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000306
307 buildbucket_put_url = (
308 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000309 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000310 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
311 hostname=rietveld_host,
312 issue=issue,
313 patch=patchset)
314
315 batch_req_body = {'builds': []}
316 print_text = []
317 print_text.append('Tried jobs on:')
318 for master, builders_and_tests in sorted(masters.iteritems()):
319 print_text.append('Master: %s' % master)
320 bucket = _prefix_master(master)
321 for builder, tests in sorted(builders_and_tests.iteritems()):
322 print_text.append(' %s: %s' % (builder, tests))
323 parameters = {
324 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000325 'changes': [{
326 'author': {'email': issue_props['owner_email']},
327 'revision': options.revision,
328 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000329 'properties': {
330 'category': category,
331 'issue': issue,
332 'master': master,
333 'patch_project': issue_props['project'],
334 'patch_storage': 'rietveld',
335 'patchset': patchset,
336 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000337 'rietveld': rietveld_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000338 },
339 }
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000340 if tests:
341 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000342 if properties:
343 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000344 if options.clobber:
345 parameters['properties']['clobber'] = True
346 batch_req_body['builds'].append(
347 {
348 'bucket': bucket,
349 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000350 'client_operation_id': str(uuid.uuid4()),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000351 'tags': ['builder:%s' % builder,
352 'buildset:%s' % buildset,
353 'master:%s' % master,
354 'user_agent:git_cl_try']
355 }
356 )
357
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000358 _buildbucket_retry(
359 'triggering tryjobs',
360 http,
361 buildbucket_put_url,
362 'PUT',
363 body=json.dumps(batch_req_body),
364 headers={'Content-Type': 'application/json'}
365 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000366 print_text.append('To see results here, run: git cl try-results')
367 print_text.append('To see results in browser, run: git cl web')
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000368 print '\n'.join(print_text)
kjellander@chromium.org44424542015-06-02 18:35:29 +0000369
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000370
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000371def fetch_try_jobs(auth_config, changelist, options):
372 """Fetches tryjobs from buildbucket.
373
374 Returns a map from build id to build info as json dictionary.
375 """
376 rietveld_url = settings.GetDefaultServerUrl()
377 rietveld_host = urlparse.urlparse(rietveld_url).hostname
378 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
379 if authenticator.has_cached_credentials():
380 http = authenticator.authorize(httplib2.Http())
381 else:
382 print ('Warning: Some results might be missing because %s' %
383 # Get the message on how to login.
384 auth.LoginRequiredError(rietveld_host).message)
385 http = httplib2.Http()
386
387 http.force_exception_to_status_code = True
388
389 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
390 hostname=rietveld_host,
391 issue=changelist.GetIssue(),
392 patch=options.patchset)
393 params = {'tag': 'buildset:%s' % buildset}
394
395 builds = {}
396 while True:
397 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
398 hostname=options.buildbucket_host,
399 params=urllib.urlencode(params))
400 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
401 for build in content.get('builds', []):
402 builds[build['id']] = build
403 if 'next_cursor' in content:
404 params['start_cursor'] = content['next_cursor']
405 else:
406 break
407 return builds
408
409
410def print_tryjobs(options, builds):
411 """Prints nicely result of fetch_try_jobs."""
412 if not builds:
413 print 'No tryjobs scheduled'
414 return
415
416 # Make a copy, because we'll be modifying builds dictionary.
417 builds = builds.copy()
418 builder_names_cache = {}
419
420 def get_builder(b):
421 try:
422 return builder_names_cache[b['id']]
423 except KeyError:
424 try:
425 parameters = json.loads(b['parameters_json'])
426 name = parameters['builder_name']
427 except (ValueError, KeyError) as error:
428 print 'WARNING: failed to get builder name for build %s: %s' % (
429 b['id'], error)
430 name = None
431 builder_names_cache[b['id']] = name
432 return name
433
434 def get_bucket(b):
435 bucket = b['bucket']
436 if bucket.startswith('master.'):
437 return bucket[len('master.'):]
438 return bucket
439
440 if options.print_master:
441 name_fmt = '%%-%ds %%-%ds' % (
442 max(len(str(get_bucket(b))) for b in builds.itervalues()),
443 max(len(str(get_builder(b))) for b in builds.itervalues()))
444 def get_name(b):
445 return name_fmt % (get_bucket(b), get_builder(b))
446 else:
447 name_fmt = '%%-%ds' % (
448 max(len(str(get_builder(b))) for b in builds.itervalues()))
449 def get_name(b):
450 return name_fmt % get_builder(b)
451
452 def sort_key(b):
453 return b['status'], b.get('result'), get_name(b), b.get('url')
454
455 def pop(title, f, color=None, **kwargs):
456 """Pop matching builds from `builds` dict and print them."""
457
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000458 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000459 colorize = str
460 else:
461 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
462
463 result = []
464 for b in builds.values():
465 if all(b.get(k) == v for k, v in kwargs.iteritems()):
466 builds.pop(b['id'])
467 result.append(b)
468 if result:
469 print colorize(title)
470 for b in sorted(result, key=sort_key):
471 print ' ', colorize('\t'.join(map(str, f(b))))
472
473 total = len(builds)
474 pop(status='COMPLETED', result='SUCCESS',
475 title='Successes:', color=Fore.GREEN,
476 f=lambda b: (get_name(b), b.get('url')))
477 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
478 title='Infra Failures:', color=Fore.MAGENTA,
479 f=lambda b: (get_name(b), b.get('url')))
480 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
481 title='Failures:', color=Fore.RED,
482 f=lambda b: (get_name(b), b.get('url')))
483 pop(status='COMPLETED', result='CANCELED',
484 title='Canceled:', color=Fore.MAGENTA,
485 f=lambda b: (get_name(b),))
486 pop(status='COMPLETED', result='FAILURE',
487 failure_reason='INVALID_BUILD_DEFINITION',
488 title='Wrong master/builder name:', color=Fore.MAGENTA,
489 f=lambda b: (get_name(b),))
490 pop(status='COMPLETED', result='FAILURE',
491 title='Other failures:',
492 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
493 pop(status='COMPLETED',
494 title='Other finished:',
495 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
496 pop(status='STARTED',
497 title='Started:', color=Fore.YELLOW,
498 f=lambda b: (get_name(b), b.get('url')))
499 pop(status='SCHEDULED',
500 title='Scheduled:',
501 f=lambda b: (get_name(b), 'id=%s' % b['id']))
502 # The last section is just in case buildbucket API changes OR there is a bug.
503 pop(title='Other:',
504 f=lambda b: (get_name(b), 'id=%s' % b['id']))
505 assert len(builds) == 0
506 print 'Total: %d tryjobs' % total
507
508
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000509def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
510 """Return the corresponding git ref if |base_url| together with |glob_spec|
511 matches the full |url|.
512
513 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
514 """
515 fetch_suburl, as_ref = glob_spec.split(':')
516 if allow_wildcards:
517 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
518 if glob_match:
519 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
520 # "branches/{472,597,648}/src:refs/remotes/svn/*".
521 branch_re = re.escape(base_url)
522 if glob_match.group(1):
523 branch_re += '/' + re.escape(glob_match.group(1))
524 wildcard = glob_match.group(2)
525 if wildcard == '*':
526 branch_re += '([^/]*)'
527 else:
528 # Escape and replace surrounding braces with parentheses and commas
529 # with pipe symbols.
530 wildcard = re.escape(wildcard)
531 wildcard = re.sub('^\\\\{', '(', wildcard)
532 wildcard = re.sub('\\\\,', '|', wildcard)
533 wildcard = re.sub('\\\\}$', ')', wildcard)
534 branch_re += wildcard
535 if glob_match.group(3):
536 branch_re += re.escape(glob_match.group(3))
537 match = re.match(branch_re, url)
538 if match:
539 return re.sub('\*$', match.group(1), as_ref)
540
541 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
542 if fetch_suburl:
543 full_url = base_url + '/' + fetch_suburl
544 else:
545 full_url = base_url
546 if full_url == url:
547 return as_ref
548 return None
549
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000550
iannucci@chromium.org79540052012-10-19 23:15:26 +0000551def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000552 """Prints statistics about the change to the user."""
553 # --no-ext-diff is broken in some versions of Git, so try to work around
554 # this by overriding the environment (but there is still a problem if the
555 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000556 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000557 if 'GIT_EXTERNAL_DIFF' in env:
558 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000559
560 if find_copies:
561 similarity_options = ['--find-copies-harder', '-l100000',
562 '-C%s' % similarity]
563 else:
564 similarity_options = ['-M%s' % similarity]
565
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000566 try:
567 stdout = sys.stdout.fileno()
568 except AttributeError:
569 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000570 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000571 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000572 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000573 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000574
575
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000576class BuildbucketResponseException(Exception):
577 pass
578
579
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000580class Settings(object):
581 def __init__(self):
582 self.default_server = None
583 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000584 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000585 self.is_git_svn = None
586 self.svn_branch = None
587 self.tree_status_url = None
588 self.viewvc_url = None
589 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000590 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000591 self.squash_gerrit_uploads = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000592 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000593 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000594 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000595 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000596
597 def LazyUpdateIfNeeded(self):
598 """Updates the settings from a codereview.settings file, if available."""
599 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000600 # The only value that actually changes the behavior is
601 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000602 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000603 error_ok=True
604 ).strip().lower()
605
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000606 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000607 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000608 LoadCodereviewSettingsFromFile(cr_settings_file)
609 self.updated = True
610
611 def GetDefaultServerUrl(self, error_ok=False):
612 if not self.default_server:
613 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000614 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000615 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000616 if error_ok:
617 return self.default_server
618 if not self.default_server:
619 error_message = ('Could not find settings file. You must configure '
620 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000621 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000622 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000623 return self.default_server
624
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000625 @staticmethod
626 def GetRelativeRoot():
627 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000628
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000629 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000630 if self.root is None:
631 self.root = os.path.abspath(self.GetRelativeRoot())
632 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000633
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000634 def GetGitMirror(self, remote='origin'):
635 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000636 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000637 if not os.path.isdir(local_url):
638 return None
639 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
640 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
641 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
642 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
643 if mirror.exists():
644 return mirror
645 return None
646
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000647 def GetIsGitSvn(self):
648 """Return true if this repo looks like it's using git-svn."""
649 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000650 if self.GetPendingRefPrefix():
651 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
652 self.is_git_svn = False
653 else:
654 # If you have any "svn-remote.*" config keys, we think you're using svn.
655 self.is_git_svn = RunGitWithCode(
656 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000657 return self.is_git_svn
658
659 def GetSVNBranch(self):
660 if self.svn_branch is None:
661 if not self.GetIsGitSvn():
662 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
663
664 # Try to figure out which remote branch we're based on.
665 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000666 # 1) iterate through our branch history and find the svn URL.
667 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000668
669 # regexp matching the git-svn line that contains the URL.
670 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
671
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000672 # We don't want to go through all of history, so read a line from the
673 # pipe at a time.
674 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000675 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000676 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
677 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000678 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000679 for line in proc.stdout:
680 match = git_svn_re.match(line)
681 if match:
682 url = match.group(1)
683 proc.stdout.close() # Cut pipe.
684 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000685
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000686 if url:
687 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
688 remotes = RunGit(['config', '--get-regexp',
689 r'^svn-remote\..*\.url']).splitlines()
690 for remote in remotes:
691 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000692 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000693 remote = match.group(1)
694 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000695 rewrite_root = RunGit(
696 ['config', 'svn-remote.%s.rewriteRoot' % remote],
697 error_ok=True).strip()
698 if rewrite_root:
699 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000700 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000701 ['config', 'svn-remote.%s.fetch' % remote],
702 error_ok=True).strip()
703 if fetch_spec:
704 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
705 if self.svn_branch:
706 break
707 branch_spec = RunGit(
708 ['config', 'svn-remote.%s.branches' % remote],
709 error_ok=True).strip()
710 if branch_spec:
711 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
712 if self.svn_branch:
713 break
714 tag_spec = RunGit(
715 ['config', 'svn-remote.%s.tags' % remote],
716 error_ok=True).strip()
717 if tag_spec:
718 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
719 if self.svn_branch:
720 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000721
722 if not self.svn_branch:
723 DieWithError('Can\'t guess svn branch -- try specifying it on the '
724 'command line')
725
726 return self.svn_branch
727
728 def GetTreeStatusUrl(self, error_ok=False):
729 if not self.tree_status_url:
730 error_message = ('You must configure your tree status URL by running '
731 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000732 self.tree_status_url = self._GetRietveldConfig(
733 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000734 return self.tree_status_url
735
736 def GetViewVCUrl(self):
737 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000738 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000739 return self.viewvc_url
740
rmistry@google.com90752582014-01-14 21:04:50 +0000741 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000742 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000743
rmistry@google.com78948ed2015-07-08 23:09:57 +0000744 def GetIsSkipDependencyUpload(self, branch_name):
745 """Returns true if specified branch should skip dep uploads."""
746 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
747 error_ok=True)
748
rmistry@google.com5626a922015-02-26 14:03:30 +0000749 def GetRunPostUploadHook(self):
750 run_post_upload_hook = self._GetRietveldConfig(
751 'run-post-upload-hook', error_ok=True)
752 return run_post_upload_hook == "True"
753
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000754 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000755 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000756
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000757 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000758 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000759
ukai@chromium.orge8077812012-02-03 03:41:46 +0000760 def GetIsGerrit(self):
761 """Return true if this repo is assosiated with gerrit code review system."""
762 if self.is_gerrit is None:
763 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
764 return self.is_gerrit
765
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000766 def GetSquashGerritUploads(self):
767 """Return true if uploads to Gerrit should be squashed by default."""
768 if self.squash_gerrit_uploads is None:
769 self.squash_gerrit_uploads = (
770 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
771 error_ok=True).strip() == 'true')
772 return self.squash_gerrit_uploads
773
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000774 def GetGitEditor(self):
775 """Return the editor specified in the git config, or None if none is."""
776 if self.git_editor is None:
777 self.git_editor = self._GetConfig('core.editor', error_ok=True)
778 return self.git_editor or None
779
thestig@chromium.org44202a22014-03-11 19:22:18 +0000780 def GetLintRegex(self):
781 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
782 DEFAULT_LINT_REGEX)
783
784 def GetLintIgnoreRegex(self):
785 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
786 DEFAULT_LINT_IGNORE_REGEX)
787
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000788 def GetProject(self):
789 if not self.project:
790 self.project = self._GetRietveldConfig('project', error_ok=True)
791 return self.project
792
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000793 def GetForceHttpsCommitUrl(self):
794 if not self.force_https_commit_url:
795 self.force_https_commit_url = self._GetRietveldConfig(
796 'force-https-commit-url', error_ok=True)
797 return self.force_https_commit_url
798
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000799 def GetPendingRefPrefix(self):
800 if not self.pending_ref_prefix:
801 self.pending_ref_prefix = self._GetRietveldConfig(
802 'pending-ref-prefix', error_ok=True)
803 return self.pending_ref_prefix
804
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000805 def _GetRietveldConfig(self, param, **kwargs):
806 return self._GetConfig('rietveld.' + param, **kwargs)
807
rmistry@google.com78948ed2015-07-08 23:09:57 +0000808 def _GetBranchConfig(self, branch_name, param, **kwargs):
809 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
810
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000811 def _GetConfig(self, param, **kwargs):
812 self.LazyUpdateIfNeeded()
813 return RunGit(['config', param], **kwargs).strip()
814
815
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000816def ShortBranchName(branch):
817 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000818 return branch.replace('refs/heads/', '', 1)
819
820
821def GetCurrentBranchRef():
822 """Returns branch ref (e.g., refs/heads/master) or None."""
823 return RunGit(['symbolic-ref', 'HEAD'],
824 stderr=subprocess2.VOID, error_ok=True).strip() or None
825
826
827def GetCurrentBranch():
828 """Returns current branch or None.
829
830 For refs/heads/* branches, returns just last part. For others, full ref.
831 """
832 branchref = GetCurrentBranchRef()
833 if branchref:
834 return ShortBranchName(branchref)
835 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000836
837
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000838class _CQState(object):
839 """Enum for states of CL with respect to Commit Queue."""
840 NONE = 'none'
841 DRY_RUN = 'dry_run'
842 COMMIT = 'commit'
843
844 ALL_STATES = [NONE, DRY_RUN, COMMIT]
845
846
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000847class _ParsedIssueNumberArgument(object):
848 def __init__(self, issue=None, patchset=None, hostname=None):
849 self.issue = issue
850 self.patchset = patchset
851 self.hostname = hostname
852
853 @property
854 def valid(self):
855 return self.issue is not None
856
857
858class _RietveldParsedIssueNumberArgument(_ParsedIssueNumberArgument):
859 def __init__(self, *args, **kwargs):
860 self.patch_url = kwargs.pop('patch_url', None)
861 super(_RietveldParsedIssueNumberArgument, self).__init__(*args, **kwargs)
862
863
864def ParseIssueNumberArgument(arg):
865 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
866 fail_result = _ParsedIssueNumberArgument()
867
868 if arg.isdigit():
869 return _ParsedIssueNumberArgument(issue=int(arg))
870 if not arg.startswith('http'):
871 return fail_result
872 url = gclient_utils.UpgradeToHttps(arg)
873 try:
874 parsed_url = urlparse.urlparse(url)
875 except ValueError:
876 return fail_result
877 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
878 tmp = cls.ParseIssueURL(parsed_url)
879 if tmp is not None:
880 return tmp
881 return fail_result
882
883
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000884class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000885 """Changelist works with one changelist in local branch.
886
887 Supports two codereview backends: Rietveld or Gerrit, selected at object
888 creation.
889
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +0000890 Notes:
891 * Not safe for concurrent multi-{thread,process} use.
892 * Caches values from current branch. Therefore, re-use after branch change
893 with care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000894 """
895
896 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
897 """Create a new ChangeList instance.
898
899 If issue is given, the codereview must be given too.
900
901 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
902 Otherwise, it's decided based on current configuration of the local branch,
903 with default being 'rietveld' for backwards compatibility.
904 See _load_codereview_impl for more details.
905
906 **kwargs will be passed directly to codereview implementation.
907 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000908 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000909 global settings
910 if not settings:
911 # Happens when git_cl.py is used as a utility library.
912 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000913
914 if issue:
915 assert codereview, 'codereview must be known, if issue is known'
916
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000917 self.branchref = branchref
918 if self.branchref:
919 self.branch = ShortBranchName(self.branchref)
920 else:
921 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000922 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000923 self.lookedup_issue = False
924 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000925 self.has_description = False
926 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000927 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000928 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000929 self.cc = None
930 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000931 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000932
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000933 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000934 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000935 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000936 assert self._codereview_impl
937 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000938
939 def _load_codereview_impl(self, codereview=None, **kwargs):
940 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000941 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
942 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
943 self._codereview = codereview
944 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000945 return
946
947 # Automatic selection based on issue number set for a current branch.
948 # Rietveld takes precedence over Gerrit.
949 assert not self.issue
950 # Whether we find issue or not, we are doing the lookup.
951 self.lookedup_issue = True
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000952 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000953 setting = cls.IssueSetting(self.GetBranch())
954 issue = RunGit(['config', setting], error_ok=True).strip()
955 if issue:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000956 self._codereview = codereview
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000957 self._codereview_impl = cls(self, **kwargs)
958 self.issue = int(issue)
959 return
960
961 # No issue is set for this branch, so decide based on repo-wide settings.
962 return self._load_codereview_impl(
963 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
964 **kwargs)
965
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000966 def IsGerrit(self):
967 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000968
969 def GetCCList(self):
970 """Return the users cc'd on this CL.
971
972 Return is a string suitable for passing to gcl with the --cc flag.
973 """
974 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000975 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000976 more_cc = ','.join(self.watchers)
977 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
978 return self.cc
979
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000980 def GetCCListWithoutDefault(self):
981 """Return the users cc'd on this CL excluding default ones."""
982 if self.cc is None:
983 self.cc = ','.join(self.watchers)
984 return self.cc
985
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000986 def SetWatchers(self, watchers):
987 """Set the list of email addresses that should be cc'd based on the changed
988 files in this CL.
989 """
990 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000991
992 def GetBranch(self):
993 """Returns the short branch name, e.g. 'master'."""
994 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000995 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000996 if not branchref:
997 return None
998 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000999 self.branch = ShortBranchName(self.branchref)
1000 return self.branch
1001
1002 def GetBranchRef(self):
1003 """Returns the full branch name, e.g. 'refs/heads/master'."""
1004 self.GetBranch() # Poke the lazy loader.
1005 return self.branchref
1006
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001007 def ClearBranch(self):
1008 """Clears cached branch data of this object."""
1009 self.branch = self.branchref = None
1010
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001011 @staticmethod
1012 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001013 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001014 e.g. 'origin', 'refs/heads/master'
1015 """
1016 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001017 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
1018 error_ok=True).strip()
1019 if upstream_branch:
1020 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
1021 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001022 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1023 error_ok=True).strip()
1024 if upstream_branch:
1025 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001026 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001027 # Fall back on trying a git-svn upstream branch.
1028 if settings.GetIsGitSvn():
1029 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001030 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001031 # Else, try to guess the origin remote.
1032 remote_branches = RunGit(['branch', '-r']).split()
1033 if 'origin/master' in remote_branches:
1034 # Fall back on origin/master if it exits.
1035 remote = 'origin'
1036 upstream_branch = 'refs/heads/master'
1037 elif 'origin/trunk' in remote_branches:
1038 # Fall back on origin/trunk if it exists. Generally a shared
1039 # git-svn clone
1040 remote = 'origin'
1041 upstream_branch = 'refs/heads/trunk'
1042 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001043 DieWithError(
1044 'Unable to determine default branch to diff against.\n'
1045 'Either pass complete "git diff"-style arguments, like\n'
1046 ' git cl upload origin/master\n'
1047 'or verify this branch is set up to track another \n'
1048 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001049
1050 return remote, upstream_branch
1051
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001052 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001053 upstream_branch = self.GetUpstreamBranch()
1054 if not BranchExists(upstream_branch):
1055 DieWithError('The upstream for the current branch (%s) does not exist '
1056 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001057 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001058 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001059
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001060 def GetUpstreamBranch(self):
1061 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001062 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001063 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001064 upstream_branch = upstream_branch.replace('refs/heads/',
1065 'refs/remotes/%s/' % remote)
1066 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1067 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001068 self.upstream_branch = upstream_branch
1069 return self.upstream_branch
1070
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001071 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001072 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001073 remote, branch = None, self.GetBranch()
1074 seen_branches = set()
1075 while branch not in seen_branches:
1076 seen_branches.add(branch)
1077 remote, branch = self.FetchUpstreamTuple(branch)
1078 branch = ShortBranchName(branch)
1079 if remote != '.' or branch.startswith('refs/remotes'):
1080 break
1081 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001082 remotes = RunGit(['remote'], error_ok=True).split()
1083 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001084 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001085 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001086 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001087 logging.warning('Could not determine which remote this change is '
1088 'associated with, so defaulting to "%s". This may '
1089 'not be what you want. You may prevent this message '
1090 'by running "git svn info" as documented here: %s',
1091 self._remote,
1092 GIT_INSTRUCTIONS_URL)
1093 else:
1094 logging.warn('Could not determine which remote this change is '
1095 'associated with. You may prevent this message by '
1096 'running "git svn info" as documented here: %s',
1097 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001098 branch = 'HEAD'
1099 if branch.startswith('refs/remotes'):
1100 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001101 elif branch.startswith('refs/branch-heads/'):
1102 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001103 else:
1104 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001105 return self._remote
1106
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001107 def GitSanityChecks(self, upstream_git_obj):
1108 """Checks git repo status and ensures diff is from local commits."""
1109
sbc@chromium.org79706062015-01-14 21:18:12 +00001110 if upstream_git_obj is None:
1111 if self.GetBranch() is None:
1112 print >> sys.stderr, (
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00001113 'ERROR: unable to determine current branch (detached HEAD?)')
sbc@chromium.org79706062015-01-14 21:18:12 +00001114 else:
1115 print >> sys.stderr, (
1116 'ERROR: no upstream branch')
1117 return False
1118
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001119 # Verify the commit we're diffing against is in our current branch.
1120 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1121 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1122 if upstream_sha != common_ancestor:
1123 print >> sys.stderr, (
1124 'ERROR: %s is not in the current branch. You may need to rebase '
1125 'your tracking branch' % upstream_sha)
1126 return False
1127
1128 # List the commits inside the diff, and verify they are all local.
1129 commits_in_diff = RunGit(
1130 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1131 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1132 remote_branch = remote_branch.strip()
1133 if code != 0:
1134 _, remote_branch = self.GetRemoteBranch()
1135
1136 commits_in_remote = RunGit(
1137 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1138
1139 common_commits = set(commits_in_diff) & set(commits_in_remote)
1140 if common_commits:
1141 print >> sys.stderr, (
1142 'ERROR: Your diff contains %d commits already in %s.\n'
1143 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1144 'the diff. If you are using a custom git flow, you can override'
1145 ' the reference used for this check with "git config '
1146 'gitcl.remotebranch <git-ref>".' % (
1147 len(common_commits), remote_branch, upstream_git_obj))
1148 return False
1149 return True
1150
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001151 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001152 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001153
1154 Returns None if it is not set.
1155 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001156 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
1157 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001158
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001159 def GetGitSvnRemoteUrl(self):
1160 """Return the configured git-svn remote URL parsed from git svn info.
1161
1162 Returns None if it is not set.
1163 """
1164 # URL is dependent on the current directory.
1165 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1166 if data:
1167 keys = dict(line.split(': ', 1) for line in data.splitlines()
1168 if ': ' in line)
1169 return keys.get('URL', None)
1170 return None
1171
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001172 def GetRemoteUrl(self):
1173 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1174
1175 Returns None if there is no remote.
1176 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001177 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001178 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1179
1180 # If URL is pointing to a local directory, it is probably a git cache.
1181 if os.path.isdir(url):
1182 url = RunGit(['config', 'remote.%s.url' % remote],
1183 error_ok=True,
1184 cwd=url).strip()
1185 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001186
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001187 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001188 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001189 if self.issue is None and not self.lookedup_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001190 issue = RunGit(['config',
1191 self._codereview_impl.IssueSetting(self.GetBranch())],
1192 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001193 self.issue = int(issue) or None if issue else None
1194 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001195 return self.issue
1196
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001197 def GetIssueURL(self):
1198 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001199 issue = self.GetIssue()
1200 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001201 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001202 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001203
1204 def GetDescription(self, pretty=False):
1205 if not self.has_description:
1206 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001207 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001208 self.has_description = True
1209 if pretty:
1210 wrapper = textwrap.TextWrapper()
1211 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1212 return wrapper.fill(self.description)
1213 return self.description
1214
1215 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001216 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001217 if self.patchset is None and not self.lookedup_patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001218 patchset = RunGit(['config', self._codereview_impl.PatchsetSetting()],
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001219 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001220 self.patchset = int(patchset) or None if patchset else None
1221 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001222 return self.patchset
1223
1224 def SetPatchset(self, patchset):
1225 """Set this branch's patchset. If patchset=0, clears the patchset."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001226 patchset_setting = self._codereview_impl.PatchsetSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001227 if patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001228 RunGit(['config', patchset_setting, str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001229 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001230 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001231 RunGit(['config', '--unset', patchset_setting],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001232 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001233 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001234
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001235 def SetIssue(self, issue=None):
1236 """Set this branch's issue. If issue isn't given, clears the issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001237 issue_setting = self._codereview_impl.IssueSetting(self.GetBranch())
1238 codereview_setting = self._codereview_impl.GetCodereviewServerSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001239 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001240 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001241 RunGit(['config', issue_setting, str(issue)])
1242 codereview_server = self._codereview_impl.GetCodereviewServer()
1243 if codereview_server:
1244 RunGit(['config', codereview_setting, codereview_server])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001245 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +00001246 current_issue = self.GetIssue()
1247 if current_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001248 RunGit(['config', '--unset', issue_setting])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001249 self.issue = None
1250 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001251
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001252 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001253 if not self.GitSanityChecks(upstream_branch):
1254 DieWithError('\nGit sanity check failure')
1255
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001256 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001257 if not root:
1258 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001259 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001260
1261 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001262 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001263 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001264 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001265 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001266 except subprocess2.CalledProcessError:
1267 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001268 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001269 'This branch probably doesn\'t exist anymore. To reset the\n'
1270 'tracking branch, please run\n'
1271 ' git branch --set-upstream %s trunk\n'
1272 'replacing trunk with origin/master or the relevant branch') %
1273 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001274
maruel@chromium.org52424302012-08-29 15:14:30 +00001275 issue = self.GetIssue()
1276 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001277 if issue:
1278 description = self.GetDescription()
1279 else:
1280 # If the change was never uploaded, use the log messages of all commits
1281 # up to the branch point, as git cl upload will prefill the description
1282 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001283 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1284 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001285
1286 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001287 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001288 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001289 name,
1290 description,
1291 absroot,
1292 files,
1293 issue,
1294 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001295 author,
1296 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001297
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001298 def UpdateDescription(self, description):
1299 self.description = description
1300 return self._codereview_impl.UpdateDescriptionRemote(description)
1301
1302 def RunHook(self, committing, may_prompt, verbose, change):
1303 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1304 try:
1305 return presubmit_support.DoPresubmitChecks(change, committing,
1306 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1307 default_presubmit=None, may_prompt=may_prompt,
1308 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit())
1309 except presubmit_support.PresubmitFailure, e:
1310 DieWithError(
1311 ('%s\nMaybe your depot_tools is out of date?\n'
1312 'If all fails, contact maruel@') % e)
1313
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001314 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1315 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001316 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1317 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001318 else:
1319 # Assume url.
1320 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1321 urlparse.urlparse(issue_arg))
1322 if not parsed_issue_arg or not parsed_issue_arg.valid:
1323 DieWithError('Failed to parse issue argument "%s". '
1324 'Must be an issue number or a valid URL.' % issue_arg)
1325 return self._codereview_impl.CMDPatchWithParsedIssue(
1326 parsed_issue_arg, reject, nocommit, directory)
1327
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001328 def CMDUpload(self, options, git_diff_args, orig_args):
1329 """Uploads a change to codereview."""
1330 if git_diff_args:
1331 # TODO(ukai): is it ok for gerrit case?
1332 base_branch = git_diff_args[0]
1333 else:
1334 if self.GetBranch() is None:
1335 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1336
1337 # Default to diffing against common ancestor of upstream branch
1338 base_branch = self.GetCommonAncestorWithUpstream()
1339 git_diff_args = [base_branch, 'HEAD']
1340
1341 # Make sure authenticated to codereview before running potentially expensive
1342 # hooks. It is a fast, best efforts check. Codereview still can reject the
1343 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001344 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001345
1346 # Apply watchlists on upload.
1347 change = self.GetChange(base_branch, None)
1348 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1349 files = [f.LocalPath() for f in change.AffectedFiles()]
1350 if not options.bypass_watchlists:
1351 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1352
1353 if not options.bypass_hooks:
1354 if options.reviewers or options.tbr_owners:
1355 # Set the reviewer list now so that presubmit checks can access it.
1356 change_description = ChangeDescription(change.FullDescriptionText())
1357 change_description.update_reviewers(options.reviewers,
1358 options.tbr_owners,
1359 change)
1360 change.SetDescriptionText(change_description.description)
1361 hook_results = self.RunHook(committing=False,
1362 may_prompt=not options.force,
1363 verbose=options.verbose,
1364 change=change)
1365 if not hook_results.should_continue():
1366 return 1
1367 if not options.reviewers and hook_results.reviewers:
1368 options.reviewers = hook_results.reviewers.split(',')
1369
1370 if self.GetIssue():
1371 latest_patchset = self.GetMostRecentPatchset()
1372 local_patchset = self.GetPatchset()
1373 if (latest_patchset and local_patchset and
1374 local_patchset != latest_patchset):
1375 print ('The last upload made from this repository was patchset #%d but '
1376 'the most recent patchset on the server is #%d.'
1377 % (local_patchset, latest_patchset))
1378 print ('Uploading will still work, but if you\'ve uploaded to this '
1379 'issue from another machine or branch the patch you\'re '
1380 'uploading now might not include those changes.')
1381 ask_for_data('About to upload; enter to confirm.')
1382
1383 print_stats(options.similarity, options.find_copies, git_diff_args)
1384 ret = self.CMDUploadChange(options, git_diff_args, change)
1385 if not ret:
1386 git_set_branch_value('last-upload-hash',
1387 RunGit(['rev-parse', 'HEAD']).strip())
1388 # Run post upload hooks, if specified.
1389 if settings.GetRunPostUploadHook():
1390 presubmit_support.DoPostUploadExecuter(
1391 change,
1392 self,
1393 settings.GetRoot(),
1394 options.verbose,
1395 sys.stdout)
1396
1397 # Upload all dependencies if specified.
1398 if options.dependencies:
1399 print
1400 print '--dependencies has been specified.'
1401 print 'All dependent local branches will be re-uploaded.'
1402 print
1403 # Remove the dependencies flag from args so that we do not end up in a
1404 # loop.
1405 orig_args.remove('--dependencies')
1406 ret = upload_branch_deps(self, orig_args)
1407 return ret
1408
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001409 def SetCQState(self, new_state):
1410 """Update the CQ state for latest patchset.
1411
1412 Issue must have been already uploaded and known.
1413 """
1414 assert new_state in _CQState.ALL_STATES
1415 assert self.GetIssue()
1416 return self._codereview_impl.SetCQState(new_state)
1417
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001418 # Forward methods to codereview specific implementation.
1419
1420 def CloseIssue(self):
1421 return self._codereview_impl.CloseIssue()
1422
1423 def GetStatus(self):
1424 return self._codereview_impl.GetStatus()
1425
1426 def GetCodereviewServer(self):
1427 return self._codereview_impl.GetCodereviewServer()
1428
1429 def GetApprovingReviewers(self):
1430 return self._codereview_impl.GetApprovingReviewers()
1431
1432 def GetMostRecentPatchset(self):
1433 return self._codereview_impl.GetMostRecentPatchset()
1434
1435 def __getattr__(self, attr):
1436 # This is because lots of untested code accesses Rietveld-specific stuff
1437 # directly, and it's hard to fix for sure. So, just let it work, and fix
1438 # on a cases by case basis.
1439 return getattr(self._codereview_impl, attr)
1440
1441
1442class _ChangelistCodereviewBase(object):
1443 """Abstract base class encapsulating codereview specifics of a changelist."""
1444 def __init__(self, changelist):
1445 self._changelist = changelist # instance of Changelist
1446
1447 def __getattr__(self, attr):
1448 # Forward methods to changelist.
1449 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1450 # _RietveldChangelistImpl to avoid this hack?
1451 return getattr(self._changelist, attr)
1452
1453 def GetStatus(self):
1454 """Apply a rough heuristic to give a simple summary of an issue's review
1455 or CQ status, assuming adherence to a common workflow.
1456
1457 Returns None if no issue for this branch, or specific string keywords.
1458 """
1459 raise NotImplementedError()
1460
1461 def GetCodereviewServer(self):
1462 """Returns server URL without end slash, like "https://codereview.com"."""
1463 raise NotImplementedError()
1464
1465 def FetchDescription(self):
1466 """Fetches and returns description from the codereview server."""
1467 raise NotImplementedError()
1468
1469 def GetCodereviewServerSetting(self):
1470 """Returns git config setting for the codereview server."""
1471 raise NotImplementedError()
1472
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001473 @classmethod
1474 def IssueSetting(cls, branch):
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001475 return 'branch.%s.%s' % (branch, cls.IssueSettingSuffix())
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001476
1477 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001478 def IssueSettingSuffix(cls):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001479 """Returns name of git config setting which stores issue number for a given
1480 branch."""
1481 raise NotImplementedError()
1482
1483 def PatchsetSetting(self):
1484 """Returns name of git config setting which stores issue number."""
1485 raise NotImplementedError()
1486
1487 def GetRieveldObjForPresubmit(self):
1488 # This is an unfortunate Rietveld-embeddedness in presubmit.
1489 # For non-Rietveld codereviews, this probably should return a dummy object.
1490 raise NotImplementedError()
1491
1492 def UpdateDescriptionRemote(self, description):
1493 """Update the description on codereview site."""
1494 raise NotImplementedError()
1495
1496 def CloseIssue(self):
1497 """Closes the issue."""
1498 raise NotImplementedError()
1499
1500 def GetApprovingReviewers(self):
1501 """Returns a list of reviewers approving the change.
1502
1503 Note: not necessarily committers.
1504 """
1505 raise NotImplementedError()
1506
1507 def GetMostRecentPatchset(self):
1508 """Returns the most recent patchset number from the codereview site."""
1509 raise NotImplementedError()
1510
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001511 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1512 directory):
1513 """Fetches and applies the issue.
1514
1515 Arguments:
1516 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1517 reject: if True, reject the failed patch instead of switching to 3-way
1518 merge. Rietveld only.
1519 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1520 only.
1521 directory: switch to directory before applying the patch. Rietveld only.
1522 """
1523 raise NotImplementedError()
1524
1525 @staticmethod
1526 def ParseIssueURL(parsed_url):
1527 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1528 failed."""
1529 raise NotImplementedError()
1530
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001531 def EnsureAuthenticated(self, force):
1532 """Best effort check that user is authenticated with codereview server.
1533
1534 Arguments:
1535 force: whether to skip confirmation questions.
1536 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001537 raise NotImplementedError()
1538
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001539 def CMDUploadChange(self, options, args, change):
1540 """Uploads a change to codereview."""
1541 raise NotImplementedError()
1542
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001543 def SetCQState(self, new_state):
1544 """Update the CQ state for latest patchset.
1545
1546 Issue must have been already uploaded and known.
1547 """
1548 raise NotImplementedError()
1549
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001550
1551class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1552 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1553 super(_RietveldChangelistImpl, self).__init__(changelist)
1554 assert settings, 'must be initialized in _ChangelistCodereviewBase'
1555 settings.GetDefaultServerUrl()
1556
1557 self._rietveld_server = rietveld_server
1558 self._auth_config = auth_config
1559 self._props = None
1560 self._rpc_server = None
1561
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001562 def GetCodereviewServer(self):
1563 if not self._rietveld_server:
1564 # If we're on a branch then get the server potentially associated
1565 # with that branch.
1566 if self.GetIssue():
1567 rietveld_server_setting = self.GetCodereviewServerSetting()
1568 if rietveld_server_setting:
1569 self._rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1570 ['config', rietveld_server_setting], error_ok=True).strip())
1571 if not self._rietveld_server:
1572 self._rietveld_server = settings.GetDefaultServerUrl()
1573 return self._rietveld_server
1574
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001575 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001576 """Best effort check that user is authenticated with Rietveld server."""
1577 if self._auth_config.use_oauth2:
1578 authenticator = auth.get_authenticator_for_host(
1579 self.GetCodereviewServer(), self._auth_config)
1580 if not authenticator.has_cached_credentials():
1581 raise auth.LoginRequiredError(self.GetCodereviewServer())
1582
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001583 def FetchDescription(self):
1584 issue = self.GetIssue()
1585 assert issue
1586 try:
1587 return self.RpcServer().get_description(issue).strip()
1588 except urllib2.HTTPError as e:
1589 if e.code == 404:
1590 DieWithError(
1591 ('\nWhile fetching the description for issue %d, received a '
1592 '404 (not found)\n'
1593 'error. It is likely that you deleted this '
1594 'issue on the server. If this is the\n'
1595 'case, please run\n\n'
1596 ' git cl issue 0\n\n'
1597 'to clear the association with the deleted issue. Then run '
1598 'this command again.') % issue)
1599 else:
1600 DieWithError(
1601 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1602 except urllib2.URLError as e:
1603 print >> sys.stderr, (
1604 'Warning: Failed to retrieve CL description due to network '
1605 'failure.')
1606 return ''
1607
1608 def GetMostRecentPatchset(self):
1609 return self.GetIssueProperties()['patchsets'][-1]
1610
1611 def GetPatchSetDiff(self, issue, patchset):
1612 return self.RpcServer().get(
1613 '/download/issue%s_%s.diff' % (issue, patchset))
1614
1615 def GetIssueProperties(self):
1616 if self._props is None:
1617 issue = self.GetIssue()
1618 if not issue:
1619 self._props = {}
1620 else:
1621 self._props = self.RpcServer().get_issue_properties(issue, True)
1622 return self._props
1623
1624 def GetApprovingReviewers(self):
1625 return get_approving_reviewers(self.GetIssueProperties())
1626
1627 def AddComment(self, message):
1628 return self.RpcServer().add_comment(self.GetIssue(), message)
1629
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001630 def GetStatus(self):
1631 """Apply a rough heuristic to give a simple summary of an issue's review
1632 or CQ status, assuming adherence to a common workflow.
1633
1634 Returns None if no issue for this branch, or one of the following keywords:
1635 * 'error' - error from review tool (including deleted issues)
1636 * 'unsent' - not sent for review
1637 * 'waiting' - waiting for review
1638 * 'reply' - waiting for owner to reply to review
1639 * 'lgtm' - LGTM from at least one approved reviewer
1640 * 'commit' - in the commit queue
1641 * 'closed' - closed
1642 """
1643 if not self.GetIssue():
1644 return None
1645
1646 try:
1647 props = self.GetIssueProperties()
1648 except urllib2.HTTPError:
1649 return 'error'
1650
1651 if props.get('closed'):
1652 # Issue is closed.
1653 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001654 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001655 # Issue is in the commit queue.
1656 return 'commit'
1657
1658 try:
1659 reviewers = self.GetApprovingReviewers()
1660 except urllib2.HTTPError:
1661 return 'error'
1662
1663 if reviewers:
1664 # Was LGTM'ed.
1665 return 'lgtm'
1666
1667 messages = props.get('messages') or []
1668
1669 if not messages:
1670 # No message was sent.
1671 return 'unsent'
1672 if messages[-1]['sender'] != props.get('owner_email'):
1673 # Non-LGTM reply from non-owner
1674 return 'reply'
1675 return 'waiting'
1676
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001677 def UpdateDescriptionRemote(self, description):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001678 return self.RpcServer().update_description(
1679 self.GetIssue(), self.description)
1680
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001681 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001682 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001683
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001684 def SetFlag(self, flag, value):
1685 """Patchset must match."""
1686 if not self.GetPatchset():
1687 DieWithError('The patchset needs to match. Send another patchset.')
1688 try:
1689 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001690 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001691 except urllib2.HTTPError, e:
1692 if e.code == 404:
1693 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1694 if e.code == 403:
1695 DieWithError(
1696 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1697 'match?') % (self.GetIssue(), self.GetPatchset()))
1698 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001699
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001700 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001701 """Returns an upload.RpcServer() to access this review's rietveld instance.
1702 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001703 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001704 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001705 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001706 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001707 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001708
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001709 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001710 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001711 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001712
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001713 def PatchsetSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001714 """Return the git setting that stores this change's most recent patchset."""
1715 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1716
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001717 def GetCodereviewServerSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001718 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001719 branch = self.GetBranch()
1720 if branch:
1721 return 'branch.%s.rietveldserver' % branch
1722 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001723
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001724 def GetRieveldObjForPresubmit(self):
1725 return self.RpcServer()
1726
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001727 def SetCQState(self, new_state):
1728 props = self.GetIssueProperties()
1729 if props.get('private'):
1730 DieWithError('Cannot set-commit on private issue')
1731
1732 if new_state == _CQState.COMMIT:
1733 self.SetFlag('commit', '1')
1734 elif new_state == _CQState.NONE:
1735 self.SetFlag('commit', '0')
1736 else:
1737 raise NotImplementedError()
1738
1739
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001740 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1741 directory):
1742 # TODO(maruel): Use apply_issue.py
1743
1744 # PatchIssue should never be called with a dirty tree. It is up to the
1745 # caller to check this, but just in case we assert here since the
1746 # consequences of the caller not checking this could be dire.
1747 assert(not git_common.is_dirty_git_tree('apply'))
1748 assert(parsed_issue_arg.valid)
1749 self._changelist.issue = parsed_issue_arg.issue
1750 if parsed_issue_arg.hostname:
1751 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1752
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001753 if (isinstance(parsed_issue_arg, _RietveldParsedIssueNumberArgument) and
1754 parsed_issue_arg.patch_url):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001755 assert parsed_issue_arg.patchset
1756 patchset = parsed_issue_arg.patchset
1757 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1758 else:
1759 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1760 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1761
1762 # Switch up to the top-level directory, if necessary, in preparation for
1763 # applying the patch.
1764 top = settings.GetRelativeRoot()
1765 if top:
1766 os.chdir(top)
1767
1768 # Git patches have a/ at the beginning of source paths. We strip that out
1769 # with a sed script rather than the -p flag to patch so we can feed either
1770 # Git or svn-style patches into the same apply command.
1771 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1772 try:
1773 patch_data = subprocess2.check_output(
1774 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1775 except subprocess2.CalledProcessError:
1776 DieWithError('Git patch mungling failed.')
1777 logging.info(patch_data)
1778
1779 # We use "git apply" to apply the patch instead of "patch" so that we can
1780 # pick up file adds.
1781 # The --index flag means: also insert into the index (so we catch adds).
1782 cmd = ['git', 'apply', '--index', '-p0']
1783 if directory:
1784 cmd.extend(('--directory', directory))
1785 if reject:
1786 cmd.append('--reject')
1787 elif IsGitVersionAtLeast('1.7.12'):
1788 cmd.append('--3way')
1789 try:
1790 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
1791 stdin=patch_data, stdout=subprocess2.VOID)
1792 except subprocess2.CalledProcessError:
1793 print 'Failed to apply the patch'
1794 return 1
1795
1796 # If we had an issue, commit the current state and register the issue.
1797 if not nocommit:
1798 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
1799 'patch from issue %(i)s at patchset '
1800 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
1801 % {'i': self.GetIssue(), 'p': patchset})])
1802 self.SetIssue(self.GetIssue())
1803 self.SetPatchset(patchset)
1804 print "Committed patch locally."
1805 else:
1806 print "Patch applied to index."
1807 return 0
1808
1809 @staticmethod
1810 def ParseIssueURL(parsed_url):
1811 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1812 return None
1813 # Typical url: https://domain/<issue_number>[/[other]]
1814 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
1815 if match:
1816 return _RietveldParsedIssueNumberArgument(
1817 issue=int(match.group(1)),
1818 hostname=parsed_url.netloc)
1819 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
1820 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
1821 if match:
1822 return _RietveldParsedIssueNumberArgument(
1823 issue=int(match.group(1)),
1824 patchset=int(match.group(2)),
1825 hostname=parsed_url.netloc,
1826 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
1827 return None
1828
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001829 def CMDUploadChange(self, options, args, change):
1830 """Upload the patch to Rietveld."""
1831 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1832 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001833 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
1834 if options.emulate_svn_auto_props:
1835 upload_args.append('--emulate_svn_auto_props')
1836
1837 change_desc = None
1838
1839 if options.email is not None:
1840 upload_args.extend(['--email', options.email])
1841
1842 if self.GetIssue():
1843 if options.title:
1844 upload_args.extend(['--title', options.title])
1845 if options.message:
1846 upload_args.extend(['--message', options.message])
1847 upload_args.extend(['--issue', str(self.GetIssue())])
1848 print ('This branch is associated with issue %s. '
1849 'Adding patch to that issue.' % self.GetIssue())
1850 else:
1851 if options.title:
1852 upload_args.extend(['--title', options.title])
1853 message = (options.title or options.message or
1854 CreateDescriptionFromLog(args))
1855 change_desc = ChangeDescription(message)
1856 if options.reviewers or options.tbr_owners:
1857 change_desc.update_reviewers(options.reviewers,
1858 options.tbr_owners,
1859 change)
1860 if not options.force:
1861 change_desc.prompt()
1862
1863 if not change_desc.description:
1864 print "Description is empty; aborting."
1865 return 1
1866
1867 upload_args.extend(['--message', change_desc.description])
1868 if change_desc.get_reviewers():
1869 upload_args.append('--reviewers=%s' % ','.join(
1870 change_desc.get_reviewers()))
1871 if options.send_mail:
1872 if not change_desc.get_reviewers():
1873 DieWithError("Must specify reviewers to send email.")
1874 upload_args.append('--send_mail')
1875
1876 # We check this before applying rietveld.private assuming that in
1877 # rietveld.cc only addresses which we can send private CLs to are listed
1878 # if rietveld.private is set, and so we should ignore rietveld.cc only
1879 # when --private is specified explicitly on the command line.
1880 if options.private:
1881 logging.warn('rietveld.cc is ignored since private flag is specified. '
1882 'You need to review and add them manually if necessary.')
1883 cc = self.GetCCListWithoutDefault()
1884 else:
1885 cc = self.GetCCList()
1886 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
1887 if cc:
1888 upload_args.extend(['--cc', cc])
1889
1890 if options.private or settings.GetDefaultPrivateFlag() == "True":
1891 upload_args.append('--private')
1892
1893 upload_args.extend(['--git_similarity', str(options.similarity)])
1894 if not options.find_copies:
1895 upload_args.extend(['--git_no_find_copies'])
1896
1897 # Include the upstream repo's URL in the change -- this is useful for
1898 # projects that have their source spread across multiple repos.
1899 remote_url = self.GetGitBaseUrlFromConfig()
1900 if not remote_url:
1901 if settings.GetIsGitSvn():
1902 remote_url = self.GetGitSvnRemoteUrl()
1903 else:
1904 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
1905 remote_url = '%s@%s' % (self.GetRemoteUrl(),
1906 self.GetUpstreamBranch().split('/')[-1])
1907 if remote_url:
1908 upload_args.extend(['--base_url', remote_url])
1909 remote, remote_branch = self.GetRemoteBranch()
1910 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
1911 settings.GetPendingRefPrefix())
1912 if target_ref:
1913 upload_args.extend(['--target_ref', target_ref])
1914
1915 # Look for dependent patchsets. See crbug.com/480453 for more details.
1916 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
1917 upstream_branch = ShortBranchName(upstream_branch)
1918 if remote is '.':
1919 # A local branch is being tracked.
1920 local_branch = ShortBranchName(upstream_branch)
1921 if settings.GetIsSkipDependencyUpload(local_branch):
1922 print
1923 print ('Skipping dependency patchset upload because git config '
1924 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
1925 print
1926 else:
1927 auth_config = auth.extract_auth_config_from_options(options)
1928 branch_cl = Changelist(branchref=local_branch,
1929 auth_config=auth_config)
1930 branch_cl_issue_url = branch_cl.GetIssueURL()
1931 branch_cl_issue = branch_cl.GetIssue()
1932 branch_cl_patchset = branch_cl.GetPatchset()
1933 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
1934 upload_args.extend(
1935 ['--depends_on_patchset', '%s:%s' % (
1936 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001937 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001938 '\n'
1939 'The current branch (%s) is tracking a local branch (%s) with '
1940 'an associated CL.\n'
1941 'Adding %s/#ps%s as a dependency patchset.\n'
1942 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
1943 branch_cl_patchset))
1944
1945 project = settings.GetProject()
1946 if project:
1947 upload_args.extend(['--project', project])
1948
1949 if options.cq_dry_run:
1950 upload_args.extend(['--cq_dry_run'])
1951
1952 try:
1953 upload_args = ['upload'] + upload_args + args
1954 logging.info('upload.RealMain(%s)', upload_args)
1955 issue, patchset = upload.RealMain(upload_args)
1956 issue = int(issue)
1957 patchset = int(patchset)
1958 except KeyboardInterrupt:
1959 sys.exit(1)
1960 except:
1961 # If we got an exception after the user typed a description for their
1962 # change, back up the description before re-raising.
1963 if change_desc:
1964 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1965 print('\nGot exception while uploading -- saving description to %s\n' %
1966 backup_path)
1967 backup_file = open(backup_path, 'w')
1968 backup_file.write(change_desc.description)
1969 backup_file.close()
1970 raise
1971
1972 if not self.GetIssue():
1973 self.SetIssue(issue)
1974 self.SetPatchset(patchset)
1975
1976 if options.use_commit_queue:
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001977 self.SetCQState(_CQState.COMMIT)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001978 return 0
1979
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001980
1981class _GerritChangelistImpl(_ChangelistCodereviewBase):
1982 def __init__(self, changelist, auth_config=None):
1983 # auth_config is Rietveld thing, kept here to preserve interface only.
1984 super(_GerritChangelistImpl, self).__init__(changelist)
1985 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001986 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001987 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001988 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001989
1990 def _GetGerritHost(self):
1991 # Lazy load of configs.
1992 self.GetCodereviewServer()
1993 return self._gerrit_host
1994
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001995 def _GetGitHost(self):
1996 """Returns git host to be used when uploading change to Gerrit."""
1997 return urlparse.urlparse(self.GetRemoteUrl()).netloc
1998
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001999 def GetCodereviewServer(self):
2000 if not self._gerrit_server:
2001 # If we're on a branch then get the server potentially associated
2002 # with that branch.
2003 if self.GetIssue():
2004 gerrit_server_setting = self.GetCodereviewServerSetting()
2005 if gerrit_server_setting:
2006 self._gerrit_server = RunGit(['config', gerrit_server_setting],
2007 error_ok=True).strip()
2008 if self._gerrit_server:
2009 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
2010 if not self._gerrit_server:
2011 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2012 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002013 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002014 parts[0] = parts[0] + '-review'
2015 self._gerrit_host = '.'.join(parts)
2016 self._gerrit_server = 'https://%s' % self._gerrit_host
2017 return self._gerrit_server
2018
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002019 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00002020 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002021 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002022
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002023 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002024 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002025 # Lazy-loader to identify Gerrit and Git hosts.
2026 if gerrit_util.GceAuthenticator.is_gce():
2027 return
2028 self.GetCodereviewServer()
2029 git_host = self._GetGitHost()
2030 assert self._gerrit_server and self._gerrit_host
2031 cookie_auth = gerrit_util.CookiesAuthenticator()
2032
2033 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2034 git_auth = cookie_auth.get_auth_header(git_host)
2035 if gerrit_auth and git_auth:
2036 if gerrit_auth == git_auth:
2037 return
2038 print((
2039 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2040 ' Check your %s or %s file for credentials of hosts:\n'
2041 ' %s\n'
2042 ' %s\n'
2043 ' %s') %
2044 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2045 git_host, self._gerrit_host,
2046 cookie_auth.get_new_password_message(git_host)))
2047 if not force:
2048 ask_for_data('If you know what you are doing, press Enter to continue, '
2049 'Ctrl+C to abort.')
2050 return
2051 else:
2052 missing = (
2053 [] if gerrit_auth else [self._gerrit_host] +
2054 [] if git_auth else [git_host])
2055 DieWithError('Credentials for the following hosts are required:\n'
2056 ' %s\n'
2057 'These are read from %s (or legacy %s)\n'
2058 '%s' % (
2059 '\n '.join(missing),
2060 cookie_auth.get_gitcookies_path(),
2061 cookie_auth.get_netrc_path(),
2062 cookie_auth.get_new_password_message(git_host)))
2063
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002064
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002065 def PatchsetSetting(self):
2066 """Return the git setting that stores this change's most recent patchset."""
2067 return 'branch.%s.gerritpatchset' % self.GetBranch()
2068
2069 def GetCodereviewServerSetting(self):
2070 """Returns the git setting that stores this change's Gerrit server."""
2071 branch = self.GetBranch()
2072 if branch:
2073 return 'branch.%s.gerritserver' % branch
2074 return None
2075
2076 def GetRieveldObjForPresubmit(self):
2077 class ThisIsNotRietveldIssue(object):
2078 def __nonzero__(self):
2079 # This is a hack to make presubmit_support think that rietveld is not
2080 # defined, yet still ensure that calls directly result in a decent
2081 # exception message below.
2082 return False
2083
2084 def __getattr__(self, attr):
2085 print(
2086 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2087 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2088 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2089 'or use Rietveld for codereview.\n'
2090 'See also http://crbug.com/579160.' % attr)
2091 raise NotImplementedError()
2092 return ThisIsNotRietveldIssue()
2093
2094 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002095 """Apply a rough heuristic to give a simple summary of an issue's review
2096 or CQ status, assuming adherence to a common workflow.
2097
2098 Returns None if no issue for this branch, or one of the following keywords:
2099 * 'error' - error from review tool (including deleted issues)
2100 * 'unsent' - no reviewers added
2101 * 'waiting' - waiting for review
2102 * 'reply' - waiting for owner to reply to review
2103 * 'not lgtm' - Code-Review -2 from at least one approved reviewer
2104 * 'lgtm' - Code-Review +2 from at least one approved reviewer
2105 * 'commit' - in the commit queue
2106 * 'closed' - abandoned
2107 """
2108 if not self.GetIssue():
2109 return None
2110
2111 try:
2112 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
2113 except httplib.HTTPException:
2114 return 'error'
2115
2116 if data['status'] == 'ABANDONED':
2117 return 'closed'
2118
2119 cq_label = data['labels'].get('Commit-Queue', {})
2120 if cq_label:
2121 # Vote value is a stringified integer, which we expect from 0 to 2.
2122 vote_value = cq_label.get('value', '0')
2123 vote_text = cq_label.get('values', {}).get(vote_value, '')
2124 if vote_text.lower() == 'commit':
2125 return 'commit'
2126
2127 lgtm_label = data['labels'].get('Code-Review', {})
2128 if lgtm_label:
2129 if 'rejected' in lgtm_label:
2130 return 'not lgtm'
2131 if 'approved' in lgtm_label:
2132 return 'lgtm'
2133
2134 if not data.get('reviewers', {}).get('REVIEWER', []):
2135 return 'unsent'
2136
2137 messages = data.get('messages', [])
2138 if messages:
2139 owner = data['owner'].get('_account_id')
2140 last_message_author = messages[-1].get('author', {}).get('_account_id')
2141 if owner != last_message_author:
2142 # Some reply from non-owner.
2143 return 'reply'
2144
2145 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002146
2147 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002148 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002149 return data['revisions'][data['current_revision']]['_number']
2150
2151 def FetchDescription(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002152 data = self._GetChangeDetail(['COMMIT_FOOTERS', 'CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002153 return data['revisions'][data['current_revision']]['commit_with_footers']
2154
2155 def UpdateDescriptionRemote(self, description):
2156 # TODO(tandrii)
2157 raise NotImplementedError()
2158
2159 def CloseIssue(self):
2160 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2161
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002162 def SubmitIssue(self, wait_for_merge=True):
2163 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2164 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002165
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002166 def _GetChangeDetail(self, options=None, issue=None):
2167 options = options or []
2168 issue = issue or self.GetIssue()
2169 assert issue, 'issue required to query Gerrit'
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002170 return gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2171 options)
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002172
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002173 def CMDLand(self, force, bypass_hooks, verbose):
2174 if git_common.is_dirty_git_tree('land'):
2175 return 1
2176 differs = True
2177 last_upload = RunGit(['config',
2178 'branch.%s.gerritsquashhash' % self.GetBranch()],
2179 error_ok=True).strip()
2180 # Note: git diff outputs nothing if there is no diff.
2181 if not last_upload or RunGit(['diff', last_upload]).strip():
2182 print('WARNING: some changes from local branch haven\'t been uploaded')
2183 else:
2184 detail = self._GetChangeDetail(['CURRENT_REVISION'])
2185 if detail['current_revision'] == last_upload:
2186 differs = False
2187 else:
2188 print('WARNING: local branch contents differ from latest uploaded '
2189 'patchset')
2190 if differs:
2191 if not force:
2192 ask_for_data(
2193 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2194 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2195 elif not bypass_hooks:
2196 hook_results = self.RunHook(
2197 committing=True,
2198 may_prompt=not force,
2199 verbose=verbose,
2200 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2201 if not hook_results.should_continue():
2202 return 1
2203
2204 self.SubmitIssue(wait_for_merge=True)
2205 print('Issue %s has been submitted.' % self.GetIssueURL())
2206 return 0
2207
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002208 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2209 directory):
2210 assert not reject
2211 assert not nocommit
2212 assert not directory
2213 assert parsed_issue_arg.valid
2214
2215 self._changelist.issue = parsed_issue_arg.issue
2216
2217 if parsed_issue_arg.hostname:
2218 self._gerrit_host = parsed_issue_arg.hostname
2219 self._gerrit_server = 'https://%s' % self._gerrit_host
2220
2221 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2222
2223 if not parsed_issue_arg.patchset:
2224 # Use current revision by default.
2225 revision_info = detail['revisions'][detail['current_revision']]
2226 patchset = int(revision_info['_number'])
2227 else:
2228 patchset = parsed_issue_arg.patchset
2229 for revision_info in detail['revisions'].itervalues():
2230 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2231 break
2232 else:
2233 DieWithError('Couldn\'t find patchset %i in issue %i' %
2234 (parsed_issue_arg.patchset, self.GetIssue()))
2235
2236 fetch_info = revision_info['fetch']['http']
2237 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2238 RunGit(['cherry-pick', 'FETCH_HEAD'])
2239 self.SetIssue(self.GetIssue())
2240 self.SetPatchset(patchset)
2241 print('Committed patch for issue %i pathset %i locally' %
2242 (self.GetIssue(), self.GetPatchset()))
2243 return 0
2244
2245 @staticmethod
2246 def ParseIssueURL(parsed_url):
2247 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2248 return None
2249 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2250 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2251 # Short urls like https://domain/<issue_number> can be used, but don't allow
2252 # specifying the patchset (you'd 404), but we allow that here.
2253 if parsed_url.path == '/':
2254 part = parsed_url.fragment
2255 else:
2256 part = parsed_url.path
2257 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2258 if match:
2259 return _ParsedIssueNumberArgument(
2260 issue=int(match.group(2)),
2261 patchset=int(match.group(4)) if match.group(4) else None,
2262 hostname=parsed_url.netloc)
2263 return None
2264
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002265 def CMDUploadChange(self, options, args, change):
2266 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002267 if options.squash and options.no_squash:
2268 DieWithError('Can only use one of --squash or --no-squash')
2269 options.squash = ((settings.GetSquashGerritUploads() or options.squash) and
2270 not options.no_squash)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002271 # We assume the remote called "origin" is the one we want.
2272 # It is probably not worthwhile to support different workflows.
2273 gerrit_remote = 'origin'
2274
2275 remote, remote_branch = self.GetRemoteBranch()
2276 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2277 pending_prefix='')
2278
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002279 if options.squash:
2280 if not self.GetIssue():
2281 # TODO(tandrii): deperecate this after 2016Q2. Backwards compatibility
2282 # with shadow branch, which used to contain change-id for a given
2283 # branch, using which we can fetch actual issue number and set it as the
2284 # property of the branch, which is the new way.
2285 message = RunGitSilent([
2286 'show', '--format=%B', '-s',
2287 'refs/heads/git_cl_uploads/%s' % self.GetBranch()])
2288 if message:
2289 change_ids = git_footers.get_footer_change_id(message.strip())
2290 if change_ids and len(change_ids) == 1:
2291 details = self._GetChangeDetail(issue=change_ids[0])
2292 if details:
2293 print('WARNING: found old upload in branch git_cl_uploads/%s '
2294 'corresponding to issue %s' %
2295 (self.GetBranch(), details['_number']))
2296 self.SetIssue(details['_number'])
2297 if not self.GetIssue():
2298 DieWithError(
2299 '\n' # For readability of the blob below.
2300 'Found old upload in branch git_cl_uploads/%s, '
2301 'but failed to find corresponding Gerrit issue.\n'
2302 'If you know the issue number, set it manually first:\n'
2303 ' git cl issue 123456\n'
2304 'If you intended to upload this CL as new issue, '
2305 'just delete or rename the old upload branch:\n'
2306 ' git rename-branch git_cl_uploads/%s old_upload-%s\n'
2307 'After that, please run git cl upload again.' %
2308 tuple([self.GetBranch()] * 3))
2309 # End of backwards compatability.
2310
2311 if self.GetIssue():
2312 # Try to get the message from a previous upload.
2313 message = self.GetDescription()
2314 if not message:
2315 DieWithError(
2316 'failed to fetch description from current Gerrit issue %d\n'
2317 '%s' % (self.GetIssue(), self.GetIssueURL()))
2318 change_id = self._GetChangeDetail()['change_id']
2319 while True:
2320 footer_change_ids = git_footers.get_footer_change_id(message)
2321 if footer_change_ids == [change_id]:
2322 break
2323 if not footer_change_ids:
2324 message = git_footers.add_footer_change_id(message, change_id)
2325 print('WARNING: appended missing Change-Id to issue description')
2326 continue
2327 # There is already a valid footer but with different or several ids.
2328 # Doing this automatically is non-trivial as we don't want to lose
2329 # existing other footers, yet we want to append just 1 desired
2330 # Change-Id. Thus, just create a new footer, but let user verify the
2331 # new description.
2332 message = '%s\n\nChange-Id: %s' % (message, change_id)
2333 print(
2334 'WARNING: issue %s has Change-Id footer(s):\n'
2335 ' %s\n'
2336 'but issue has Change-Id %s, according to Gerrit.\n'
2337 'Please, check the proposed correction to the description, '
2338 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2339 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2340 change_id))
2341 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2342 if not options.force:
2343 change_desc = ChangeDescription(message)
2344 change_desc.prompt()
2345 message = change_desc.description
2346 if not message:
2347 DieWithError("Description is empty. Aborting...")
2348 # Continue the while loop.
2349 # Sanity check of this code - we should end up with proper message
2350 # footer.
2351 assert [change_id] == git_footers.get_footer_change_id(message)
2352 change_desc = ChangeDescription(message)
2353 else:
2354 change_desc = ChangeDescription(
2355 options.message or CreateDescriptionFromLog(args))
2356 if not options.force:
2357 change_desc.prompt()
2358 if not change_desc.description:
2359 DieWithError("Description is empty. Aborting...")
2360 message = change_desc.description
2361 change_ids = git_footers.get_footer_change_id(message)
2362 if len(change_ids) > 1:
2363 DieWithError('too many Change-Id footers, at most 1 allowed.')
2364 if not change_ids:
2365 # Generate the Change-Id automatically.
2366 message = git_footers.add_footer_change_id(
2367 message, GenerateGerritChangeId(message))
2368 change_desc.set_description(message)
2369 change_ids = git_footers.get_footer_change_id(message)
2370 assert len(change_ids) == 1
2371 change_id = change_ids[0]
2372
2373 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2374 if remote is '.':
2375 # If our upstream branch is local, we base our squashed commit on its
2376 # squashed version.
2377 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2378 # Check the squashed hash of the parent.
2379 parent = RunGit(['config',
2380 'branch.%s.gerritsquashhash' % upstream_branch_name],
2381 error_ok=True).strip()
2382 # Verify that the upstream branch has been uploaded too, otherwise
2383 # Gerrit will create additional CLs when uploading.
2384 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2385 RunGitSilent(['rev-parse', parent + ':'])):
2386 # TODO(tandrii): remove "old depot_tools" part on April 12, 2016.
2387 DieWithError(
2388 'Upload upstream branch %s first.\n'
2389 'Note: maybe you\'ve uploaded it with --no-squash or with an old '
2390 'version of depot_tools. If so, then re-upload it with:\n'
2391 ' git cl upload --squash\n' % upstream_branch_name)
2392 else:
2393 parent = self.GetCommonAncestorWithUpstream()
2394
2395 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2396 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2397 '-m', message]).strip()
2398 else:
2399 change_desc = ChangeDescription(
2400 options.message or CreateDescriptionFromLog(args))
2401 if not change_desc.description:
2402 DieWithError("Description is empty. Aborting...")
2403
2404 if not git_footers.get_footer_change_id(change_desc.description):
2405 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002406 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2407 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002408 ref_to_push = 'HEAD'
2409 parent = '%s/%s' % (gerrit_remote, branch)
2410 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2411
2412 assert change_desc
2413 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2414 ref_to_push)]).splitlines()
2415 if len(commits) > 1:
2416 print('WARNING: This will upload %d commits. Run the following command '
2417 'to see which commits will be uploaded: ' % len(commits))
2418 print('git log %s..%s' % (parent, ref_to_push))
2419 print('You can also use `git squash-branch` to squash these into a '
2420 'single commit.')
2421 ask_for_data('About to upload; enter to confirm.')
2422
2423 if options.reviewers or options.tbr_owners:
2424 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2425 change)
2426
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002427 # Extra options that can be specified at push time. Doc:
2428 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2429 refspec_opts = []
2430 if options.title:
2431 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2432 # reverse on its side.
2433 if '_' in options.title:
2434 print('WARNING: underscores in title will be converted to spaces.')
2435 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2436
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002437 cc = self.GetCCList().split(',')
2438 if options.cc:
2439 cc.extend(options.cc)
2440 cc = filter(None, cc)
2441 if cc:
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002442 refspec_opts.extend('cc=' + email.strip() for email in cc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002443
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002444 if change_desc.get_reviewers():
2445 refspec_opts.extend('r=' + email.strip()
2446 for email in change_desc.get_reviewers())
2447
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002448
2449 refspec_suffix = ''
2450 if refspec_opts:
2451 refspec_suffix = '%' + ','.join(refspec_opts)
2452 assert ' ' not in refspec_suffix, (
2453 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002454 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002455
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002456 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002457 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002458 print_stdout=True,
2459 # Flush after every line: useful for seeing progress when running as
2460 # recipe.
2461 filter_fn=lambda _: sys.stdout.flush())
2462
2463 if options.squash:
2464 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2465 change_numbers = [m.group(1)
2466 for m in map(regex.match, push_stdout.splitlines())
2467 if m]
2468 if len(change_numbers) != 1:
2469 DieWithError(
2470 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2471 'Change-Id: %s') % (len(change_numbers), change_id))
2472 self.SetIssue(change_numbers[0])
2473 RunGit(['config', 'branch.%s.gerritsquashhash' % self.GetBranch(),
2474 ref_to_push])
2475 return 0
2476
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002477 def _AddChangeIdToCommitMessage(self, options, args):
2478 """Re-commits using the current message, assumes the commit hook is in
2479 place.
2480 """
2481 log_desc = options.message or CreateDescriptionFromLog(args)
2482 git_command = ['commit', '--amend', '-m', log_desc]
2483 RunGit(git_command)
2484 new_log_desc = CreateDescriptionFromLog(args)
2485 if git_footers.get_footer_change_id(new_log_desc):
2486 print 'git-cl: Added Change-Id to commit message.'
2487 return new_log_desc
2488 else:
2489 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002490
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002491 def SetCQState(self, new_state):
2492 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
2493 # TODO(tandrii): maybe allow configurability in codereview.settings or by
2494 # self-discovery of label config for this CL using REST API.
2495 vote_map = {
2496 _CQState.NONE: 0,
2497 _CQState.DRY_RUN: 1,
2498 _CQState.COMMIT : 2,
2499 }
2500 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2501 labels={'Commit-Queue': vote_map[new_state]})
2502
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002503
2504_CODEREVIEW_IMPLEMENTATIONS = {
2505 'rietveld': _RietveldChangelistImpl,
2506 'gerrit': _GerritChangelistImpl,
2507}
2508
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002509
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002510def _add_codereview_select_options(parser):
2511 """Appends --gerrit and --rietveld options to force specific codereview."""
2512 parser.codereview_group = optparse.OptionGroup(
2513 parser, 'EXPERIMENTAL! Codereview override options')
2514 parser.add_option_group(parser.codereview_group)
2515 parser.codereview_group.add_option(
2516 '--gerrit', action='store_true',
2517 help='Force the use of Gerrit for codereview')
2518 parser.codereview_group.add_option(
2519 '--rietveld', action='store_true',
2520 help='Force the use of Rietveld for codereview')
2521
2522
2523def _process_codereview_select_options(parser, options):
2524 if options.gerrit and options.rietveld:
2525 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2526 options.forced_codereview = None
2527 if options.gerrit:
2528 options.forced_codereview = 'gerrit'
2529 elif options.rietveld:
2530 options.forced_codereview = 'rietveld'
2531
2532
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002533class ChangeDescription(object):
2534 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002535 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002536 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002537
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002538 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002539 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002540
agable@chromium.org42c20792013-09-12 17:34:49 +00002541 @property # www.logilab.org/ticket/89786
2542 def description(self): # pylint: disable=E0202
2543 return '\n'.join(self._description_lines)
2544
2545 def set_description(self, desc):
2546 if isinstance(desc, basestring):
2547 lines = desc.splitlines()
2548 else:
2549 lines = [line.rstrip() for line in desc]
2550 while lines and not lines[0]:
2551 lines.pop(0)
2552 while lines and not lines[-1]:
2553 lines.pop(-1)
2554 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002555
piman@chromium.org336f9122014-09-04 02:16:55 +00002556 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002557 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002558 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002559 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002560 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002561 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002562
agable@chromium.org42c20792013-09-12 17:34:49 +00002563 # Get the set of R= and TBR= lines and remove them from the desciption.
2564 regexp = re.compile(self.R_LINE)
2565 matches = [regexp.match(line) for line in self._description_lines]
2566 new_desc = [l for i, l in enumerate(self._description_lines)
2567 if not matches[i]]
2568 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002569
agable@chromium.org42c20792013-09-12 17:34:49 +00002570 # Construct new unified R= and TBR= lines.
2571 r_names = []
2572 tbr_names = []
2573 for match in matches:
2574 if not match:
2575 continue
2576 people = cleanup_list([match.group(2).strip()])
2577 if match.group(1) == 'TBR':
2578 tbr_names.extend(people)
2579 else:
2580 r_names.extend(people)
2581 for name in r_names:
2582 if name not in reviewers:
2583 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002584 if add_owners_tbr:
2585 owners_db = owners.Database(change.RepositoryRoot(),
2586 fopen=file, os_path=os.path, glob=glob.glob)
2587 all_reviewers = set(tbr_names + reviewers)
2588 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2589 all_reviewers)
2590 tbr_names.extend(owners_db.reviewers_for(missing_files,
2591 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002592 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2593 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2594
2595 # Put the new lines in the description where the old first R= line was.
2596 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2597 if 0 <= line_loc < len(self._description_lines):
2598 if new_tbr_line:
2599 self._description_lines.insert(line_loc, new_tbr_line)
2600 if new_r_line:
2601 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002602 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002603 if new_r_line:
2604 self.append_footer(new_r_line)
2605 if new_tbr_line:
2606 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002607
2608 def prompt(self):
2609 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002610 self.set_description([
2611 '# Enter a description of the change.',
2612 '# This will be displayed on the codereview site.',
2613 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002614 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002615 '--------------------',
2616 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002617
agable@chromium.org42c20792013-09-12 17:34:49 +00002618 regexp = re.compile(self.BUG_LINE)
2619 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00002620 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00002621 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002622 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002623 if not content:
2624 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002625 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002626
2627 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002628 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2629 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002630 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002631 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002632
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002633 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00002634 if self._description_lines:
2635 # Add an empty line if either the last line or the new line isn't a tag.
2636 last_line = self._description_lines[-1]
2637 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
2638 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2639 self._description_lines.append('')
2640 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002641
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002642 def get_reviewers(self):
2643 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002644 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
2645 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002646 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002647
2648
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002649def get_approving_reviewers(props):
2650 """Retrieves the reviewers that approved a CL from the issue properties with
2651 messages.
2652
2653 Note that the list may contain reviewers that are not committer, thus are not
2654 considered by the CQ.
2655 """
2656 return sorted(
2657 set(
2658 message['sender']
2659 for message in props['messages']
2660 if message['approval'] and message['sender'] in props['reviewers']
2661 )
2662 )
2663
2664
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002665def FindCodereviewSettingsFile(filename='codereview.settings'):
2666 """Finds the given file starting in the cwd and going up.
2667
2668 Only looks up to the top of the repository unless an
2669 'inherit-review-settings-ok' file exists in the root of the repository.
2670 """
2671 inherit_ok_file = 'inherit-review-settings-ok'
2672 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002673 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002674 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2675 root = '/'
2676 while True:
2677 if filename in os.listdir(cwd):
2678 if os.path.isfile(os.path.join(cwd, filename)):
2679 return open(os.path.join(cwd, filename))
2680 if cwd == root:
2681 break
2682 cwd = os.path.dirname(cwd)
2683
2684
2685def LoadCodereviewSettingsFromFile(fileobj):
2686 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002687 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002688
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002689 def SetProperty(name, setting, unset_error_ok=False):
2690 fullname = 'rietveld.' + name
2691 if setting in keyvals:
2692 RunGit(['config', fullname, keyvals[setting]])
2693 else:
2694 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2695
2696 SetProperty('server', 'CODE_REVIEW_SERVER')
2697 # Only server setting is required. Other settings can be absent.
2698 # In that case, we ignore errors raised during option deletion attempt.
2699 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002700 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002701 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2702 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002703 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002704 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002705 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2706 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002707 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002708 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002709 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002710 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2711 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002712
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002713 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002714 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002715
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002716 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
2717 RunGit(['config', 'gerrit.squash-uploads',
2718 keyvals['GERRIT_SQUASH_UPLOADS']])
2719
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002720 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2721 #should be of the form
2722 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2723 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2724 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2725 keyvals['ORIGIN_URL_CONFIG']])
2726
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002727
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002728def urlretrieve(source, destination):
2729 """urllib is broken for SSL connections via a proxy therefore we
2730 can't use urllib.urlretrieve()."""
2731 with open(destination, 'w') as f:
2732 f.write(urllib2.urlopen(source).read())
2733
2734
ukai@chromium.org712d6102013-11-27 00:52:58 +00002735def hasSheBang(fname):
2736 """Checks fname is a #! script."""
2737 with open(fname) as f:
2738 return f.read(2).startswith('#!')
2739
2740
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00002741# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
2742def DownloadHooks(*args, **kwargs):
2743 pass
2744
2745
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002746def DownloadGerritHook(force):
2747 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002748
2749 Args:
2750 force: True to update hooks. False to install hooks if not present.
2751 """
2752 if not settings.GetIsGerrit():
2753 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00002754 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002755 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2756 if not os.access(dst, os.X_OK):
2757 if os.path.exists(dst):
2758 if not force:
2759 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002760 try:
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002761 print(
2762 'WARNING: installing Gerrit commit-msg hook.\n'
2763 ' This behavior of git cl will soon be disabled.\n'
2764 ' See bug http://crbug.com/579176.')
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002765 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002766 if not hasSheBang(dst):
2767 DieWithError('Not a script: %s\n'
2768 'You need to download from\n%s\n'
2769 'into .git/hooks/commit-msg and '
2770 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002771 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2772 except Exception:
2773 if os.path.exists(dst):
2774 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002775 DieWithError('\nFailed to download hooks.\n'
2776 'You need to download from\n%s\n'
2777 'into .git/hooks/commit-msg and '
2778 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002779
2780
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002781
2782def GetRietveldCodereviewSettingsInteractively():
2783 """Prompt the user for settings."""
2784 server = settings.GetDefaultServerUrl(error_ok=True)
2785 prompt = 'Rietveld server (host[:port])'
2786 prompt += ' [%s]' % (server or DEFAULT_SERVER)
2787 newserver = ask_for_data(prompt + ':')
2788 if not server and not newserver:
2789 newserver = DEFAULT_SERVER
2790 if newserver:
2791 newserver = gclient_utils.UpgradeToHttps(newserver)
2792 if newserver != server:
2793 RunGit(['config', 'rietveld.server', newserver])
2794
2795 def SetProperty(initial, caption, name, is_url):
2796 prompt = caption
2797 if initial:
2798 prompt += ' ("x" to clear) [%s]' % initial
2799 new_val = ask_for_data(prompt + ':')
2800 if new_val == 'x':
2801 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
2802 elif new_val:
2803 if is_url:
2804 new_val = gclient_utils.UpgradeToHttps(new_val)
2805 if new_val != initial:
2806 RunGit(['config', 'rietveld.' + name, new_val])
2807
2808 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
2809 SetProperty(settings.GetDefaultPrivateFlag(),
2810 'Private flag (rietveld only)', 'private', False)
2811 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
2812 'tree-status-url', False)
2813 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
2814 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
2815 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
2816 'run-post-upload-hook', False)
2817
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002818@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002819def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002820 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002821
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002822 print('WARNING: git cl config works for Rietveld only.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002823 'For Gerrit, see http://crbug.com/603116.')
2824 # TODO(tandrii): add Gerrit support as part of http://crbug.com/603116.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00002825 parser.add_option('--activate-update', action='store_true',
2826 help='activate auto-updating [rietveld] section in '
2827 '.git/config')
2828 parser.add_option('--deactivate-update', action='store_true',
2829 help='deactivate auto-updating [rietveld] section in '
2830 '.git/config')
2831 options, args = parser.parse_args(args)
2832
2833 if options.deactivate_update:
2834 RunGit(['config', 'rietveld.autoupdate', 'false'])
2835 return
2836
2837 if options.activate_update:
2838 RunGit(['config', '--unset', 'rietveld.autoupdate'])
2839 return
2840
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002841 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002842 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002843 return 0
2844
2845 url = args[0]
2846 if not url.endswith('codereview.settings'):
2847 url = os.path.join(url, 'codereview.settings')
2848
2849 # Load code review settings and download hooks (if available).
2850 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
2851 return 0
2852
2853
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002854def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002855 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002856 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
2857 branch = ShortBranchName(branchref)
2858 _, args = parser.parse_args(args)
2859 if not args:
2860 print("Current base-url:")
2861 return RunGit(['config', 'branch.%s.base-url' % branch],
2862 error_ok=False).strip()
2863 else:
2864 print("Setting base-url to %s" % args[0])
2865 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
2866 error_ok=False).strip()
2867
2868
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002869def color_for_status(status):
2870 """Maps a Changelist status to color, for CMDstatus and other tools."""
2871 return {
2872 'unsent': Fore.RED,
2873 'waiting': Fore.BLUE,
2874 'reply': Fore.YELLOW,
2875 'lgtm': Fore.GREEN,
2876 'commit': Fore.MAGENTA,
2877 'closed': Fore.CYAN,
2878 'error': Fore.WHITE,
2879 }.get(status, Fore.WHITE)
2880
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002881def fetch_cl_status(branch, auth_config=None):
2882 """Fetches information for an issue and returns (branch, issue, status)."""
2883 cl = Changelist(branchref=branch, auth_config=auth_config)
2884 url = cl.GetIssueURL()
2885 status = cl.GetStatus()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002886
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002887 if url and (not status or status == 'error'):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002888 # The issue probably doesn't exist anymore.
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002889 url += ' (broken)'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002890
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002891 return (branch, url, status)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002892
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002893def get_cl_statuses(
2894 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002895 """Returns a blocking iterable of (branch, issue, color) for given branches.
2896
2897 If fine_grained is true, this will fetch CL statuses from the server.
2898 Otherwise, simply indicate if there's a matching url for the given branches.
2899
2900 If max_processes is specified, it is used as the maximum number of processes
2901 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
2902 spawned.
2903 """
2904 # Silence upload.py otherwise it becomes unwieldly.
2905 upload.verbosity = 0
2906
2907 if fine_grained:
2908 # Process one branch synchronously to work through authentication, then
2909 # spawn processes to process all the other branches in parallel.
2910 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002911 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
2912 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002913
2914 branches_to_fetch = branches[1:]
2915 pool = ThreadPool(
2916 min(max_processes, len(branches_to_fetch))
2917 if max_processes is not None
2918 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002919 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002920 yield x
2921 else:
2922 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
2923 for b in branches:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002924 cl = Changelist(branchref=b, auth_config=auth_config)
2925 url = cl.GetIssueURL()
2926 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002927
rmistry@google.com2dd99862015-06-22 12:22:18 +00002928
2929def upload_branch_deps(cl, args):
2930 """Uploads CLs of local branches that are dependents of the current branch.
2931
2932 If the local branch dependency tree looks like:
2933 test1 -> test2.1 -> test3.1
2934 -> test3.2
2935 -> test2.2 -> test3.3
2936
2937 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
2938 run on the dependent branches in this order:
2939 test2.1, test3.1, test3.2, test2.2, test3.3
2940
2941 Note: This function does not rebase your local dependent branches. Use it when
2942 you make a change to the parent branch that will not conflict with its
2943 dependent branches, and you would like their dependencies updated in
2944 Rietveld.
2945 """
2946 if git_common.is_dirty_git_tree('upload-branch-deps'):
2947 return 1
2948
2949 root_branch = cl.GetBranch()
2950 if root_branch is None:
2951 DieWithError('Can\'t find dependent branches from detached HEAD state. '
2952 'Get on a branch!')
2953 if not cl.GetIssue() or not cl.GetPatchset():
2954 DieWithError('Current branch does not have an uploaded CL. We cannot set '
2955 'patchset dependencies without an uploaded CL.')
2956
2957 branches = RunGit(['for-each-ref',
2958 '--format=%(refname:short) %(upstream:short)',
2959 'refs/heads'])
2960 if not branches:
2961 print('No local branches found.')
2962 return 0
2963
2964 # Create a dictionary of all local branches to the branches that are dependent
2965 # on it.
2966 tracked_to_dependents = collections.defaultdict(list)
2967 for b in branches.splitlines():
2968 tokens = b.split()
2969 if len(tokens) == 2:
2970 branch_name, tracked = tokens
2971 tracked_to_dependents[tracked].append(branch_name)
2972
2973 print
2974 print 'The dependent local branches of %s are:' % root_branch
2975 dependents = []
2976 def traverse_dependents_preorder(branch, padding=''):
2977 dependents_to_process = tracked_to_dependents.get(branch, [])
2978 padding += ' '
2979 for dependent in dependents_to_process:
2980 print '%s%s' % (padding, dependent)
2981 dependents.append(dependent)
2982 traverse_dependents_preorder(dependent, padding)
2983 traverse_dependents_preorder(root_branch)
2984 print
2985
2986 if not dependents:
2987 print 'There are no dependent local branches for %s' % root_branch
2988 return 0
2989
2990 print ('This command will checkout all dependent branches and run '
2991 '"git cl upload".')
2992 ask_for_data('[Press enter to continue or ctrl-C to quit]')
2993
andybons@chromium.org962f9462016-02-03 20:00:42 +00002994 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00002995 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00002996 args.extend(['-t', 'Updated patchset dependency'])
2997
rmistry@google.com2dd99862015-06-22 12:22:18 +00002998 # Record all dependents that failed to upload.
2999 failures = {}
3000 # Go through all dependents, checkout the branch and upload.
3001 try:
3002 for dependent_branch in dependents:
3003 print
3004 print '--------------------------------------'
3005 print 'Running "git cl upload" from %s:' % dependent_branch
3006 RunGit(['checkout', '-q', dependent_branch])
3007 print
3008 try:
3009 if CMDupload(OptionParser(), args) != 0:
3010 print 'Upload failed for %s!' % dependent_branch
3011 failures[dependent_branch] = 1
3012 except: # pylint: disable=W0702
3013 failures[dependent_branch] = 1
3014 print
3015 finally:
3016 # Swap back to the original root branch.
3017 RunGit(['checkout', '-q', root_branch])
3018
3019 print
3020 print 'Upload complete for dependent branches!'
3021 for dependent_branch in dependents:
3022 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
3023 print ' %s : %s' % (dependent_branch, upload_status)
3024 print
3025
3026 return 0
3027
3028
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003029def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003030 """Show status of changelists.
3031
3032 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003033 - Red not sent for review or broken
3034 - Blue waiting for review
3035 - Yellow waiting for you to reply to review
3036 - Green LGTM'ed
3037 - Magenta in the commit queue
3038 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003039
3040 Also see 'git cl comments'.
3041 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003042 parser.add_option('--field',
3043 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003044 parser.add_option('-f', '--fast', action='store_true',
3045 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003046 parser.add_option(
3047 '-j', '--maxjobs', action='store', type=int,
3048 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003049
3050 auth.add_auth_options(parser)
3051 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003052 if args:
3053 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003054 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003055
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003056 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003057 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003058 if options.field.startswith('desc'):
3059 print cl.GetDescription()
3060 elif options.field == 'id':
3061 issueid = cl.GetIssue()
3062 if issueid:
3063 print issueid
3064 elif options.field == 'patch':
3065 patchset = cl.GetPatchset()
3066 if patchset:
3067 print patchset
3068 elif options.field == 'url':
3069 url = cl.GetIssueURL()
3070 if url:
3071 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003072 return 0
3073
3074 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3075 if not branches:
3076 print('No local branch found.')
3077 return 0
3078
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003079 changes = (
3080 Changelist(branchref=b, auth_config=auth_config)
3081 for b in branches.splitlines())
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003082 # TODO(tandrii): refactor to use CLs list instead of branches list.
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00003083 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003084 alignment = max(5, max(len(b) for b in branches))
3085 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003086 output = get_cl_statuses(branches,
3087 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003088 max_processes=options.maxjobs,
3089 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003090
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003091 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003092 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003093 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003094 while branch not in branch_statuses:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003095 b, i, status = output.next()
3096 branch_statuses[b] = (i, status)
3097 issue_url, status = branch_statuses.pop(branch)
3098 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003099 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003100 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003101 color = ''
3102 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003103 status_str = '(%s)' % status if status else ''
3104 print ' %*s : %s%s %s%s' % (
3105 alignment, ShortBranchName(branch), color, issue_url, status_str,
3106 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003107
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003108 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003109 print
3110 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003111 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003112 if not cl.GetIssue():
3113 print 'No issue assigned.'
3114 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003115 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00003116 if not options.fast:
3117 print 'Issue description:'
3118 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003119 return 0
3120
3121
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003122def colorize_CMDstatus_doc():
3123 """To be called once in main() to add colors to git cl status help."""
3124 colors = [i for i in dir(Fore) if i[0].isupper()]
3125
3126 def colorize_line(line):
3127 for color in colors:
3128 if color in line.upper():
3129 # Extract whitespaces first and the leading '-'.
3130 indent = len(line) - len(line.lstrip(' ')) + 1
3131 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3132 return line
3133
3134 lines = CMDstatus.__doc__.splitlines()
3135 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3136
3137
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003138@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003139def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003140 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003141
3142 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003143 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003144 parser.add_option('-r', '--reverse', action='store_true',
3145 help='Lookup the branch(es) for the specified issues. If '
3146 'no issues are specified, all branches with mapped '
3147 'issues will be listed.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003148 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003149 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003150 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003151
dnj@chromium.org406c4402015-03-03 17:22:28 +00003152 if options.reverse:
3153 branches = RunGit(['for-each-ref', 'refs/heads',
3154 '--format=%(refname:short)']).splitlines()
3155
3156 # Reverse issue lookup.
3157 issue_branch_map = {}
3158 for branch in branches:
3159 cl = Changelist(branchref=branch)
3160 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3161 if not args:
3162 args = sorted(issue_branch_map.iterkeys())
3163 for issue in args:
3164 if not issue:
3165 continue
3166 print 'Branch for issue number %s: %s' % (
3167 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
3168 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003169 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003170 if len(args) > 0:
3171 try:
3172 issue = int(args[0])
3173 except ValueError:
3174 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003175 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003176 cl.SetIssue(issue)
3177 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003178 return 0
3179
3180
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003181def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003182 """Shows or posts review comments for any changelist."""
3183 parser.add_option('-a', '--add-comment', dest='comment',
3184 help='comment to add to an issue')
3185 parser.add_option('-i', dest='issue',
3186 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003187 parser.add_option('-j', '--json-file',
3188 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003189 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003190 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003191 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003192
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003193 issue = None
3194 if options.issue:
3195 try:
3196 issue = int(options.issue)
3197 except ValueError:
3198 DieWithError('A review issue id is expected to be a number')
3199
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003200 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003201
3202 if options.comment:
3203 cl.AddComment(options.comment)
3204 return 0
3205
3206 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003207 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003208 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003209 summary.append({
3210 'date': message['date'],
3211 'lgtm': False,
3212 'message': message['text'],
3213 'not_lgtm': False,
3214 'sender': message['sender'],
3215 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003216 if message['disapproval']:
3217 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003218 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003219 elif message['approval']:
3220 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003221 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003222 elif message['sender'] == data['owner_email']:
3223 color = Fore.MAGENTA
3224 else:
3225 color = Fore.BLUE
3226 print '\n%s%s %s%s' % (
3227 color, message['date'].split('.', 1)[0], message['sender'],
3228 Fore.RESET)
3229 if message['text'].strip():
3230 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00003231 if options.json_file:
3232 with open(options.json_file, 'wb') as f:
3233 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003234 return 0
3235
3236
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003237def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003238 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003239 parser.add_option('-d', '--display', action='store_true',
3240 help='Display the description instead of opening an editor')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003241 auth.add_auth_options(parser)
3242 options, _ = parser.parse_args(args)
3243 auth_config = auth.extract_auth_config_from_options(options)
3244 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003245 if not cl.GetIssue():
3246 DieWithError('This branch has no associated changelist.')
3247 description = ChangeDescription(cl.GetDescription())
smut@google.com34fb6b12015-07-13 20:03:26 +00003248 if options.display:
3249 print description.description
3250 return 0
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003251 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003252 if cl.GetDescription() != description.description:
3253 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003254 return 0
3255
3256
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003257def CreateDescriptionFromLog(args):
3258 """Pulls out the commit log to use as a base for the CL description."""
3259 log_args = []
3260 if len(args) == 1 and not args[0].endswith('.'):
3261 log_args = [args[0] + '..']
3262 elif len(args) == 1 and args[0].endswith('...'):
3263 log_args = [args[0][:-1]]
3264 elif len(args) == 2:
3265 log_args = [args[0] + '..' + args[1]]
3266 else:
3267 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003268 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003269
3270
thestig@chromium.org44202a22014-03-11 19:22:18 +00003271def CMDlint(parser, args):
3272 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003273 parser.add_option('--filter', action='append', metavar='-x,+y',
3274 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003275 auth.add_auth_options(parser)
3276 options, args = parser.parse_args(args)
3277 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003278
3279 # Access to a protected member _XX of a client class
3280 # pylint: disable=W0212
3281 try:
3282 import cpplint
3283 import cpplint_chromium
3284 except ImportError:
3285 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
3286 return 1
3287
3288 # Change the current working directory before calling lint so that it
3289 # shows the correct base.
3290 previous_cwd = os.getcwd()
3291 os.chdir(settings.GetRoot())
3292 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003293 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003294 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3295 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003296 if not files:
3297 print "Cannot lint an empty CL"
3298 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003299
3300 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003301 command = args + files
3302 if options.filter:
3303 command = ['--filter=' + ','.join(options.filter)] + command
3304 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003305
3306 white_regex = re.compile(settings.GetLintRegex())
3307 black_regex = re.compile(settings.GetLintIgnoreRegex())
3308 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3309 for filename in filenames:
3310 if white_regex.match(filename):
3311 if black_regex.match(filename):
3312 print "Ignoring file %s" % filename
3313 else:
3314 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3315 extra_check_functions)
3316 else:
3317 print "Skipping file %s" % filename
3318 finally:
3319 os.chdir(previous_cwd)
3320 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
3321 if cpplint._cpplint_state.error_count != 0:
3322 return 1
3323 return 0
3324
3325
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003326def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003327 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003328 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003329 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003330 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003331 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003332 auth.add_auth_options(parser)
3333 options, args = parser.parse_args(args)
3334 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003335
sbc@chromium.org71437c02015-04-09 19:29:40 +00003336 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00003337 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003338 return 1
3339
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003340 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003341 if args:
3342 base_branch = args[0]
3343 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003344 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003345 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003346
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003347 cl.RunHook(
3348 committing=not options.upload,
3349 may_prompt=False,
3350 verbose=options.verbose,
3351 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003352 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003353
3354
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003355def GenerateGerritChangeId(message):
3356 """Returns Ixxxxxx...xxx change id.
3357
3358 Works the same way as
3359 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3360 but can be called on demand on all platforms.
3361
3362 The basic idea is to generate git hash of a state of the tree, original commit
3363 message, author/committer info and timestamps.
3364 """
3365 lines = []
3366 tree_hash = RunGitSilent(['write-tree'])
3367 lines.append('tree %s' % tree_hash.strip())
3368 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3369 if code == 0:
3370 lines.append('parent %s' % parent.strip())
3371 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3372 lines.append('author %s' % author.strip())
3373 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3374 lines.append('committer %s' % committer.strip())
3375 lines.append('')
3376 # Note: Gerrit's commit-hook actually cleans message of some lines and
3377 # whitespace. This code is not doing this, but it clearly won't decrease
3378 # entropy.
3379 lines.append(message)
3380 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3381 stdin='\n'.join(lines))
3382 return 'I%s' % change_hash.strip()
3383
3384
wittman@chromium.org455dc922015-01-26 20:15:50 +00003385def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3386 """Computes the remote branch ref to use for the CL.
3387
3388 Args:
3389 remote (str): The git remote for the CL.
3390 remote_branch (str): The git remote branch for the CL.
3391 target_branch (str): The target branch specified by the user.
3392 pending_prefix (str): The pending prefix from the settings.
3393 """
3394 if not (remote and remote_branch):
3395 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003396
wittman@chromium.org455dc922015-01-26 20:15:50 +00003397 if target_branch:
3398 # Cannonicalize branch references to the equivalent local full symbolic
3399 # refs, which are then translated into the remote full symbolic refs
3400 # below.
3401 if '/' not in target_branch:
3402 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3403 else:
3404 prefix_replacements = (
3405 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3406 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3407 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3408 )
3409 match = None
3410 for regex, replacement in prefix_replacements:
3411 match = re.search(regex, target_branch)
3412 if match:
3413 remote_branch = target_branch.replace(match.group(0), replacement)
3414 break
3415 if not match:
3416 # This is a branch path but not one we recognize; use as-is.
3417 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003418 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3419 # Handle the refs that need to land in different refs.
3420 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003421
wittman@chromium.org455dc922015-01-26 20:15:50 +00003422 # Create the true path to the remote branch.
3423 # Does the following translation:
3424 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3425 # * refs/remotes/origin/master -> refs/heads/master
3426 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3427 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3428 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3429 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3430 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3431 'refs/heads/')
3432 elif remote_branch.startswith('refs/remotes/branch-heads'):
3433 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3434 # If a pending prefix exists then replace refs/ with it.
3435 if pending_prefix:
3436 remote_branch = remote_branch.replace('refs/', pending_prefix)
3437 return remote_branch
3438
3439
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003440def cleanup_list(l):
3441 """Fixes a list so that comma separated items are put as individual items.
3442
3443 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3444 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3445 """
3446 items = sum((i.split(',') for i in l), [])
3447 stripped_items = (i.strip() for i in items)
3448 return sorted(filter(None, stripped_items))
3449
3450
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003451@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003452def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003453 """Uploads the current changelist to codereview.
3454
3455 Can skip dependency patchset uploads for a branch by running:
3456 git config branch.branch_name.skip-deps-uploads True
3457 To unset run:
3458 git config --unset branch.branch_name.skip-deps-uploads
3459 Can also set the above globally by using the --global flag.
3460 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003461 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3462 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003463 parser.add_option('--bypass-watchlists', action='store_true',
3464 dest='bypass_watchlists',
3465 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003466 parser.add_option('-f', action='store_true', dest='force',
3467 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003468 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003469 parser.add_option('-t', dest='title',
3470 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003471 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003472 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003473 help='reviewer email addresses')
3474 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003475 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003476 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003477 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003478 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003479 parser.add_option('--emulate_svn_auto_props',
3480 '--emulate-svn-auto-props',
3481 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003482 dest="emulate_svn_auto_props",
3483 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003484 parser.add_option('-c', '--use-commit-queue', action='store_true',
3485 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003486 parser.add_option('--private', action='store_true',
3487 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003488 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003489 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003490 metavar='TARGET',
3491 help='Apply CL to remote ref TARGET. ' +
3492 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003493 parser.add_option('--squash', action='store_true',
3494 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003495 parser.add_option('--no-squash', action='store_true',
3496 help='Don\'t squash multiple commits into one ' +
3497 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003498 parser.add_option('--email', default=None,
3499 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003500 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3501 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003502 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3503 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003504 help='Send the patchset to do a CQ dry run right after '
3505 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003506 parser.add_option('--dependencies', action='store_true',
3507 help='Uploads CLs of all the local branches that depend on '
3508 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003509
rmistry@google.com2dd99862015-06-22 12:22:18 +00003510 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003511 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003512 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003513 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003514 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003515 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003516 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003517
sbc@chromium.org71437c02015-04-09 19:29:40 +00003518 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003519 return 1
3520
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003521 options.reviewers = cleanup_list(options.reviewers)
3522 options.cc = cleanup_list(options.cc)
3523
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003524 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3525 settings.GetIsGerrit()
3526
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003527 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00003528 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003529
3530
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003531def IsSubmoduleMergeCommit(ref):
3532 # When submodules are added to the repo, we expect there to be a single
3533 # non-git-svn merge commit at remote HEAD with a signature comment.
3534 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003535 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003536 return RunGit(cmd) != ''
3537
3538
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003539def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003540 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003541
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003542 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3543 upstream and closes the issue automatically and atomically.
3544
3545 Otherwise (in case of Rietveld):
3546 Squashes branch into a single commit.
3547 Updates changelog with metadata (e.g. pointer to review).
3548 Pushes/dcommits the code upstream.
3549 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003550 """
3551 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3552 help='bypass upload presubmit hook')
3553 parser.add_option('-m', dest='message',
3554 help="override review description")
3555 parser.add_option('-f', action='store_true', dest='force',
3556 help="force yes to questions (don't prompt)")
3557 parser.add_option('-c', dest='contributor',
3558 help="external contributor for patch (appended to " +
3559 "description and used as author for git). Should be " +
3560 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003561 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003562 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003563 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003564 auth_config = auth.extract_auth_config_from_options(options)
3565
3566 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003567
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003568 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3569 if cl.IsGerrit():
3570 if options.message:
3571 # This could be implemented, but it requires sending a new patch to
3572 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3573 # Besides, Gerrit has the ability to change the commit message on submit
3574 # automatically, thus there is no need to support this option (so far?).
3575 parser.error('-m MESSAGE option is not supported for Gerrit.')
3576 if options.contributor:
3577 parser.error(
3578 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3579 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3580 'the contributor\'s "name <email>". If you can\'t upload such a '
3581 'commit for review, contact your repository admin and request'
3582 '"Forge-Author" permission.')
3583 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3584 options.verbose)
3585
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003586 current = cl.GetBranch()
3587 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3588 if not settings.GetIsGitSvn() and remote == '.':
3589 print
3590 print 'Attempting to push branch %r into another local branch!' % current
3591 print
3592 print 'Either reparent this branch on top of origin/master:'
3593 print ' git reparent-branch --root'
3594 print
3595 print 'OR run `git rebase-update` if you think the parent branch is already'
3596 print 'committed.'
3597 print
3598 print ' Current parent: %r' % upstream_branch
3599 return 1
3600
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003601 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003602 # Default to merging against our best guess of the upstream branch.
3603 args = [cl.GetUpstreamBranch()]
3604
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003605 if options.contributor:
3606 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
3607 print "Please provide contibutor as 'First Last <email@example.com>'"
3608 return 1
3609
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003610 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003611 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003612
sbc@chromium.org71437c02015-04-09 19:29:40 +00003613 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003614 return 1
3615
3616 # This rev-list syntax means "show all commits not in my branch that
3617 # are in base_branch".
3618 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3619 base_branch]).splitlines()
3620 if upstream_commits:
3621 print ('Base branch "%s" has %d commits '
3622 'not in this branch.' % (base_branch, len(upstream_commits)))
3623 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
3624 return 1
3625
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003626 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003627 svn_head = None
3628 if cmd == 'dcommit' or base_has_submodules:
3629 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
3630 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003631
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003632 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003633 # If the base_head is a submodule merge commit, the first parent of the
3634 # base_head should be a git-svn commit, which is what we're interested in.
3635 base_svn_head = base_branch
3636 if base_has_submodules:
3637 base_svn_head += '^1'
3638
3639 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003640 if extra_commits:
3641 print ('This branch has %d additional commits not upstreamed yet.'
3642 % len(extra_commits.splitlines()))
3643 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
3644 'before attempting to %s.' % (base_branch, cmd))
3645 return 1
3646
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003647 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003648 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003649 author = None
3650 if options.contributor:
3651 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003652 hook_results = cl.RunHook(
3653 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003654 may_prompt=not options.force,
3655 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003656 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003657 if not hook_results.should_continue():
3658 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003659
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003660 # Check the tree status if the tree status URL is set.
3661 status = GetTreeStatus()
3662 if 'closed' == status:
3663 print('The tree is closed. Please wait for it to reopen. Use '
3664 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3665 return 1
3666 elif 'unknown' == status:
3667 print('Unable to determine tree status. Please verify manually and '
3668 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3669 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003670
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003671 change_desc = ChangeDescription(options.message)
3672 if not change_desc.description and cl.GetIssue():
3673 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003674
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003675 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00003676 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003677 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00003678 else:
3679 print 'No description set.'
3680 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
3681 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003682
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003683 # Keep a separate copy for the commit message, because the commit message
3684 # contains the link to the Rietveld issue, while the Rietveld message contains
3685 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003686 # Keep a separate copy for the commit message.
3687 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00003688 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003689
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003690 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00003691 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00003692 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00003693 # after it. Add a period on a new line to circumvent this. Also add a space
3694 # before the period to make sure that Gitiles continues to correctly resolve
3695 # the URL.
3696 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003697 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003698 commit_desc.append_footer('Patch from %s.' % options.contributor)
3699
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00003700 print('Description:')
3701 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003702
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003703 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003704 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00003705 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003706
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003707 # We want to squash all this branch's commits into one commit with the proper
3708 # description. We do this by doing a "reset --soft" to the base branch (which
3709 # keeps the working copy the same), then dcommitting that. If origin/master
3710 # has a submodule merge commit, we'll also need to cherry-pick the squashed
3711 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003712 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003713 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
3714 # Delete the branches if they exist.
3715 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
3716 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
3717 result = RunGitWithCode(showref_cmd)
3718 if result[0] == 0:
3719 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003720
3721 # We might be in a directory that's present in this branch but not in the
3722 # trunk. Move up to the top of the tree so that git commands that expect a
3723 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003724 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003725 if rel_base_path:
3726 os.chdir(rel_base_path)
3727
3728 # Stuff our change into the merge branch.
3729 # We wrap in a try...finally block so if anything goes wrong,
3730 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003731 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003732 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003733 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003734 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003735 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00003736 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003737 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003738 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003739 RunGit(
3740 [
3741 'commit', '--author', options.contributor,
3742 '-m', commit_desc.description,
3743 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003744 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003745 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003746 if base_has_submodules:
3747 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
3748 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
3749 RunGit(['checkout', CHERRY_PICK_BRANCH])
3750 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003751 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003752 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003753 mirror = settings.GetGitMirror(remote)
3754 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003755 pending_prefix = settings.GetPendingRefPrefix()
3756 if not pending_prefix or branch.startswith(pending_prefix):
3757 # If not using refs/pending/heads/* at all, or target ref is already set
3758 # to pending, then push to the target ref directly.
3759 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003760 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003761 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003762 else:
3763 # Cherry-pick the change on top of pending ref and then push it.
3764 assert branch.startswith('refs/'), branch
3765 assert pending_prefix[-1] == '/', pending_prefix
3766 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003767 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003768 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003769 if retcode == 0:
3770 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003771 else:
3772 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003773 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003774 'svn', 'dcommit',
3775 '-C%s' % options.similarity,
3776 '--no-rebase', '--rmdir',
3777 ]
3778 if settings.GetForceHttpsCommitUrl():
3779 # Allow forcing https commit URLs for some projects that don't allow
3780 # committing to http URLs (like Google Code).
3781 remote_url = cl.GetGitSvnRemoteUrl()
3782 if urlparse.urlparse(remote_url).scheme == 'http':
3783 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003784 cmd_args.append('--commit-url=%s' % remote_url)
3785 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003786 if 'Committed r' in output:
3787 revision = re.match(
3788 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
3789 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003790 finally:
3791 # And then swap back to the original branch and clean up.
3792 RunGit(['checkout', '-q', cl.GetBranch()])
3793 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003794 if base_has_submodules:
3795 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003796
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003797 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003798 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003799 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003800
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003801 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003802 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003803 try:
3804 revision = WaitForRealCommit(remote, revision, base_branch, branch)
3805 # We set pushed_to_pending to False, since it made it all the way to the
3806 # real ref.
3807 pushed_to_pending = False
3808 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003809 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003810
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003811 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003812 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003813 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003814 if not to_pending:
3815 if viewvc_url and revision:
3816 change_desc.append_footer(
3817 'Committed: %s%s' % (viewvc_url, revision))
3818 elif revision:
3819 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003820 print ('Closing issue '
3821 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003822 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003823 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003824 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00003825 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00003826 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00003827 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003828 if options.bypass_hooks:
3829 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
3830 else:
3831 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00003832 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003833 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003834
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003835 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003836 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
3837 print 'The commit is in the pending queue (%s).' % pending_ref
3838 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00003839 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003840 'footer.' % branch)
3841
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003842 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
3843 if os.path.isfile(hook):
3844 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003845
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003846 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003847
3848
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003849def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
3850 print
3851 print 'Waiting for commit to be landed on %s...' % real_ref
3852 print '(If you are impatient, you may Ctrl-C once without harm)'
3853 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
3854 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003855 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003856
3857 loop = 0
3858 while True:
3859 sys.stdout.write('fetching (%d)... \r' % loop)
3860 sys.stdout.flush()
3861 loop += 1
3862
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003863 if mirror:
3864 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003865 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
3866 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
3867 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
3868 for commit in commits.splitlines():
3869 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
3870 print 'Found commit on %s' % real_ref
3871 return commit
3872
3873 current_rev = to_rev
3874
3875
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003876def PushToGitPending(remote, pending_ref, upstream_ref):
3877 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
3878
3879 Returns:
3880 (retcode of last operation, output log of last operation).
3881 """
3882 assert pending_ref.startswith('refs/'), pending_ref
3883 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
3884 cherry = RunGit(['rev-parse', 'HEAD']).strip()
3885 code = 0
3886 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003887 max_attempts = 3
3888 attempts_left = max_attempts
3889 while attempts_left:
3890 if attempts_left != max_attempts:
3891 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
3892 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003893
3894 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003895 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003896 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003897 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003898 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003899 print 'Fetch failed with exit code %d.' % code
3900 if out.strip():
3901 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003902 continue
3903
3904 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003905 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003906 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003907 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003908 if code:
3909 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003910 'Your patch doesn\'t apply cleanly to ref \'%s\', '
3911 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003912 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
3913 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003914 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003915 return code, out
3916
3917 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003918 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003919 code, out = RunGitWithCode(
3920 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
3921 if code == 0:
3922 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003923 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003924 return code, out
3925
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003926 print 'Push failed with exit code %d.' % code
3927 if out.strip():
3928 print out.strip()
3929 if IsFatalPushFailure(out):
3930 print (
3931 'Fatal push error. Make sure your .netrc credentials and git '
3932 'user.email are correct and you have push access to the repo.')
3933 return code, out
3934
3935 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003936 return code, out
3937
3938
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003939def IsFatalPushFailure(push_stdout):
3940 """True if retrying push won't help."""
3941 return '(prohibited by Gerrit)' in push_stdout
3942
3943
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003944@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003945def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003946 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003947 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003948 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003949 # If it looks like previous commits were mirrored with git-svn.
3950 message = """This repository appears to be a git-svn mirror, but no
3951upstream SVN master is set. You probably need to run 'git auto-svn' once."""
3952 else:
3953 message = """This doesn't appear to be an SVN repository.
3954If your project has a true, writeable git repository, you probably want to run
3955'git cl land' instead.
3956If your project has a git mirror of an upstream SVN master, you probably need
3957to run 'git svn init'.
3958
3959Using the wrong command might cause your commit to appear to succeed, and the
3960review to be closed, without actually landing upstream. If you choose to
3961proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00003962 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00003963 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003964 return SendUpstream(parser, args, 'dcommit')
3965
3966
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003967@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003968def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003969 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003970 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003971 print('This appears to be an SVN repository.')
3972 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003973 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00003974 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003975 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003976
3977
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003978@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003979def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00003980 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003981 parser.add_option('-b', dest='newbranch',
3982 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003983 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003984 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003985 parser.add_option('-d', '--directory', action='store', metavar='DIR',
3986 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003987 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003988 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003989 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003990 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003991 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003992 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003993
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003994
3995 group = optparse.OptionGroup(
3996 parser,
3997 'Options for continuing work on the current issue uploaded from a '
3998 'different clone (e.g. different machine). Must be used independently '
3999 'from the other options. No issue number should be specified, and the '
4000 'branch must have an issue number associated with it')
4001 group.add_option('--reapply', action='store_true', dest='reapply',
4002 help='Reset the branch and reapply the issue.\n'
4003 'CAUTION: This will undo any local changes in this '
4004 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004005
4006 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004007 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004008 parser.add_option_group(group)
4009
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004010 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004011 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004012 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004013 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004014 auth_config = auth.extract_auth_config_from_options(options)
4015
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004016 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004017
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004018 issue_arg = None
4019 if options.reapply :
4020 if len(args) > 0:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004021 parser.error('--reapply implies no additional arguments.')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004022
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004023 issue_arg = cl.GetIssue()
4024 upstream = cl.GetUpstreamBranch()
4025 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004026 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004027
4028 RunGit(['reset', '--hard', upstream])
4029 if options.pull:
4030 RunGit(['pull'])
4031 else:
4032 if len(args) != 1:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004033 parser.error('Must specify issue number or url')
4034 issue_arg = args[0]
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004035
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004036 if not issue_arg:
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004037 parser.print_help()
4038 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004039
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004040 if cl.IsGerrit():
4041 if options.reject:
4042 parser.error('--reject is not supported with Gerrit codereview.')
4043 if options.nocommit:
4044 parser.error('--nocommit is not supported with Gerrit codereview.')
4045 if options.directory:
4046 parser.error('--directory is not supported with Gerrit codereview.')
4047
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004048 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004049 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004050 return 1
4051
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004052 if options.newbranch:
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004053 if options.reapply:
4054 parser.error("--reapply excludes any option other than --pull")
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004055 if options.force:
4056 RunGit(['branch', '-D', options.newbranch],
4057 stderr=subprocess2.PIPE, error_ok=True)
4058 RunGit(['checkout', '-b', options.newbranch,
4059 Changelist().GetUpstreamBranch()])
4060
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004061 return cl.CMDPatchIssue(issue_arg, options.reject, options.nocommit,
4062 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004063
4064
4065def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004066 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004067 # Provide a wrapper for git svn rebase to help avoid accidental
4068 # git svn dcommit.
4069 # It's the only command that doesn't use parser at all since we just defer
4070 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004071
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004072 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004073
4074
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004075def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004076 """Fetches the tree status and returns either 'open', 'closed',
4077 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004078 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004079 if url:
4080 status = urllib2.urlopen(url).read().lower()
4081 if status.find('closed') != -1 or status == '0':
4082 return 'closed'
4083 elif status.find('open') != -1 or status == '1':
4084 return 'open'
4085 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004086 return 'unset'
4087
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004088
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004089def GetTreeStatusReason():
4090 """Fetches the tree status from a json url and returns the message
4091 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004092 url = settings.GetTreeStatusUrl()
4093 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004094 connection = urllib2.urlopen(json_url)
4095 status = json.loads(connection.read())
4096 connection.close()
4097 return status['message']
4098
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004099
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004100def GetBuilderMaster(bot_list):
4101 """For a given builder, fetch the master from AE if available."""
4102 map_url = 'https://builders-map.appspot.com/'
4103 try:
4104 master_map = json.load(urllib2.urlopen(map_url))
4105 except urllib2.URLError as e:
4106 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4107 (map_url, e))
4108 except ValueError as e:
4109 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4110 if not master_map:
4111 return None, 'Failed to build master map.'
4112
4113 result_master = ''
4114 for bot in bot_list:
4115 builder = bot.split(':', 1)[0]
4116 master_list = master_map.get(builder, [])
4117 if not master_list:
4118 return None, ('No matching master for builder %s.' % builder)
4119 elif len(master_list) > 1:
4120 return None, ('The builder name %s exists in multiple masters %s.' %
4121 (builder, master_list))
4122 else:
4123 cur_master = master_list[0]
4124 if not result_master:
4125 result_master = cur_master
4126 elif result_master != cur_master:
4127 return None, 'The builders do not belong to the same master.'
4128 return result_master, None
4129
4130
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004131def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004132 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004133 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004134 status = GetTreeStatus()
4135 if 'unset' == status:
4136 print 'You must configure your tree status URL by running "git cl config".'
4137 return 2
4138
4139 print "The tree is %s" % status
4140 print
4141 print GetTreeStatusReason()
4142 if status != 'open':
4143 return 1
4144 return 0
4145
4146
maruel@chromium.org15192402012-09-06 12:38:29 +00004147def CMDtry(parser, args):
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004148 """Triggers try jobs through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00004149 group = optparse.OptionGroup(parser, "Try job options")
4150 group.add_option(
4151 "-b", "--bot", action="append",
4152 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
4153 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004154 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00004155 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004156 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00004157 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004158 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00004159 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004160 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004161 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004162 "-r", "--revision",
4163 help="Revision to use for the try job; default: the "
4164 "revision will be determined by the try server; see "
4165 "its waterfall for more info")
4166 group.add_option(
4167 "-c", "--clobber", action="store_true", default=False,
4168 help="Force a clobber before building; e.g. don't do an "
4169 "incremental build")
4170 group.add_option(
4171 "--project",
4172 help="Override which project to use. Projects are defined "
4173 "server-side to define what default bot set to use")
4174 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004175 "-p", "--property", dest="properties", action="append", default=[],
4176 help="Specify generic properties in the form -p key1=value1 -p "
4177 "key2=value2 etc (buildbucket only). The value will be treated as "
4178 "json if decodable, or as string otherwise.")
4179 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004180 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004181 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004182 "--use-rietveld", action="store_true", default=False,
4183 help="Use Rietveld to trigger try jobs.")
4184 group.add_option(
4185 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4186 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004187 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004188 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004189 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004190 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004191
machenbach@chromium.org45453142015-09-15 08:45:22 +00004192 if options.use_rietveld and options.properties:
4193 parser.error('Properties can only be specified with buildbucket')
4194
4195 # Make sure that all properties are prop=value pairs.
4196 bad_params = [x for x in options.properties if '=' not in x]
4197 if bad_params:
4198 parser.error('Got properties with missing "=": %s' % bad_params)
4199
maruel@chromium.org15192402012-09-06 12:38:29 +00004200 if args:
4201 parser.error('Unknown arguments: %s' % args)
4202
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004203 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004204 if not cl.GetIssue():
4205 parser.error('Need to upload first')
4206
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004207 if cl.IsGerrit():
4208 parser.error(
4209 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4210 'If your project has Commit Queue, dry run is a workaround:\n'
4211 ' git cl set-commit --dry-run')
4212 # Code below assumes Rietveld issue.
4213 # TODO(tandrii): actually implement for Gerrit http://crbug.com/599931.
4214
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004215 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004216 if props.get('closed'):
4217 parser.error('Cannot send tryjobs for a closed CL')
4218
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004219 if props.get('private'):
4220 parser.error('Cannot use trybots with private issue')
4221
maruel@chromium.org15192402012-09-06 12:38:29 +00004222 if not options.name:
4223 options.name = cl.GetBranch()
4224
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004225 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004226 options.master, err_msg = GetBuilderMaster(options.bot)
4227 if err_msg:
4228 parser.error('Tryserver master cannot be found because: %s\n'
4229 'Please manually specify the tryserver master'
4230 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004231
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004232 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004233 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004234 if not options.bot:
4235 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004236
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004237 # Get try masters from PRESUBMIT.py files.
4238 masters = presubmit_support.DoGetTryMasters(
4239 change,
4240 change.LocalPaths(),
4241 settings.GetRoot(),
4242 None,
4243 None,
4244 options.verbose,
4245 sys.stdout)
4246 if masters:
4247 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004248
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004249 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4250 options.bot = presubmit_support.DoGetTrySlaves(
4251 change,
4252 change.LocalPaths(),
4253 settings.GetRoot(),
4254 None,
4255 None,
4256 options.verbose,
4257 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004258
4259 if not options.bot:
4260 # Get try masters from cq.cfg if any.
4261 # TODO(tandrii): some (but very few) projects store cq.cfg in different
4262 # location.
4263 cq_cfg = os.path.join(change.RepositoryRoot(),
4264 'infra', 'config', 'cq.cfg')
4265 if os.path.exists(cq_cfg):
4266 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00004267 cq_masters = commit_queue.get_master_builder_map(
4268 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004269 for master, builders in cq_masters.iteritems():
4270 for builder in builders:
4271 # Skip presubmit builders, because these will fail without LGTM.
4272 if 'presubmit' not in builder.lower():
4273 masters.setdefault(master, {})[builder] = ['defaulttests']
4274 if masters:
4275 return masters
4276
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004277 if not options.bot:
4278 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00004279
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004280 builders_and_tests = {}
4281 # TODO(machenbach): The old style command-line options don't support
4282 # multiple try masters yet.
4283 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4284 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4285
4286 for bot in old_style:
4287 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004288 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004289 elif ',' in bot:
4290 parser.error('Specify one bot per --bot flag')
4291 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004292 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004293
4294 for bot, tests in new_style:
4295 builders_and_tests.setdefault(bot, []).extend(tests)
4296
4297 # Return a master map with one master to be backwards compatible. The
4298 # master name defaults to an empty string, which will cause the master
4299 # not to be set on rietveld (deprecated).
4300 return {options.master: builders_and_tests}
4301
4302 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004303
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004304 for builders in masters.itervalues():
4305 if any('triggered' in b for b in builders):
4306 print >> sys.stderr, (
4307 'ERROR You are trying to send a job to a triggered bot. This type of'
4308 ' bot requires an\ninitial job from a parent (usually a builder). '
4309 'Instead send your job to the parent.\n'
4310 'Bot list: %s' % builders)
4311 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004312
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004313 patchset = cl.GetMostRecentPatchset()
4314 if patchset and patchset != cl.GetPatchset():
4315 print(
4316 '\nWARNING Mismatch between local config and server. Did a previous '
4317 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4318 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004319 if options.luci:
4320 trigger_luci_job(cl, masters, options)
4321 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004322 try:
4323 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4324 except BuildbucketResponseException as ex:
4325 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004326 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004327 except Exception as e:
4328 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4329 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
4330 e, stacktrace)
4331 return 1
4332 else:
4333 try:
4334 cl.RpcServer().trigger_distributed_try_jobs(
4335 cl.GetIssue(), patchset, options.name, options.clobber,
4336 options.revision, masters)
4337 except urllib2.HTTPError as e:
4338 if e.code == 404:
4339 print('404 from rietveld; '
4340 'did you mean to use "git try" instead of "git cl try"?')
4341 return 1
4342 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004343
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004344 for (master, builders) in sorted(masters.iteritems()):
4345 if master:
4346 print 'Master: %s' % master
4347 length = max(len(builder) for builder in builders)
4348 for builder in sorted(builders):
4349 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00004350 return 0
4351
4352
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004353def CMDtry_results(parser, args):
4354 group = optparse.OptionGroup(parser, "Try job results options")
4355 group.add_option(
4356 "-p", "--patchset", type=int, help="patchset number if not current.")
4357 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004358 "--print-master", action='store_true', help="print master name as well.")
4359 group.add_option(
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004360 "--color", action='store_true', default=setup_color.IS_TTY,
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004361 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004362 group.add_option(
4363 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4364 help="Host of buildbucket. The default host is %default.")
4365 parser.add_option_group(group)
4366 auth.add_auth_options(parser)
4367 options, args = parser.parse_args(args)
4368 if args:
4369 parser.error('Unrecognized args: %s' % ' '.join(args))
4370
4371 auth_config = auth.extract_auth_config_from_options(options)
4372 cl = Changelist(auth_config=auth_config)
4373 if not cl.GetIssue():
4374 parser.error('Need to upload first')
4375
4376 if not options.patchset:
4377 options.patchset = cl.GetMostRecentPatchset()
4378 if options.patchset and options.patchset != cl.GetPatchset():
4379 print(
4380 '\nWARNING Mismatch between local config and server. Did a previous '
4381 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4382 'Continuing using\npatchset %s.\n' % options.patchset)
4383 try:
4384 jobs = fetch_try_jobs(auth_config, cl, options)
4385 except BuildbucketResponseException as ex:
4386 print 'Buildbucket error: %s' % ex
4387 return 1
4388 except Exception as e:
4389 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4390 print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
4391 e, stacktrace)
4392 return 1
4393 print_tryjobs(options, jobs)
4394 return 0
4395
4396
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004397@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004398def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004399 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004400 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004401 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004402 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004403
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004404 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004405 if args:
4406 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004407 branch = cl.GetBranch()
4408 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004409 cl = Changelist()
4410 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004411
4412 # Clear configured merge-base, if there is one.
4413 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004414 else:
4415 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004416 return 0
4417
4418
thestig@chromium.org00858c82013-12-02 23:08:03 +00004419def CMDweb(parser, args):
4420 """Opens the current CL in the web browser."""
4421 _, args = parser.parse_args(args)
4422 if args:
4423 parser.error('Unrecognized args: %s' % ' '.join(args))
4424
4425 issue_url = Changelist().GetIssueURL()
4426 if not issue_url:
4427 print >> sys.stderr, 'ERROR No issue to open'
4428 return 1
4429
4430 webbrowser.open(issue_url)
4431 return 0
4432
4433
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004434def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004435 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004436 parser.add_option('-d', '--dry-run', action='store_true',
4437 help='trigger in dry run mode')
4438 parser.add_option('-c', '--clear', action='store_true',
4439 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004440 auth.add_auth_options(parser)
4441 options, args = parser.parse_args(args)
4442 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004443 if args:
4444 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004445 if options.dry_run and options.clear:
4446 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4447
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004448 cl = Changelist(auth_config=auth_config)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004449 if options.clear:
4450 state = _CQState.CLEAR
4451 elif options.dry_run:
4452 state = _CQState.DRY_RUN
4453 else:
4454 state = _CQState.COMMIT
4455 if not cl.GetIssue():
4456 parser.error('Must upload the issue first')
4457 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004458 return 0
4459
4460
groby@chromium.org411034a2013-02-26 15:12:01 +00004461def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004462 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004463 auth.add_auth_options(parser)
4464 options, args = parser.parse_args(args)
4465 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004466 if args:
4467 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004468 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00004469 # Ensure there actually is an issue to close.
4470 cl.GetDescription()
4471 cl.CloseIssue()
4472 return 0
4473
4474
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004475def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004476 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004477 auth.add_auth_options(parser)
4478 options, args = parser.parse_args(args)
4479 auth_config = auth.extract_auth_config_from_options(options)
4480 if args:
4481 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004482
4483 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004484 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004485 # Staged changes would be committed along with the patch from last
4486 # upload, hence counted toward the "last upload" side in the final
4487 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004488 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004489 return 1
4490
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004491 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004492 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004493 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004494 if not issue:
4495 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004496 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004497 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004498
4499 # Create a new branch based on the merge-base
4500 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004501 # Clear cached branch in cl object, to avoid overwriting original CL branch
4502 # properties.
4503 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004504 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004505 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004506 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004507 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004508 return rtn
4509
wychen@chromium.org06928532015-02-03 02:11:29 +00004510 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004511 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00004512 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004513 finally:
4514 RunGit(['checkout', '-q', branch])
4515 RunGit(['branch', '-D', TMP_BRANCH])
4516
4517 return 0
4518
4519
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004520def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004521 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004522 parser.add_option(
4523 '--no-color',
4524 action='store_true',
4525 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004526 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004527 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004528 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004529
4530 author = RunGit(['config', 'user.email']).strip() or None
4531
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004532 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004533
4534 if args:
4535 if len(args) > 1:
4536 parser.error('Unknown args')
4537 base_branch = args[0]
4538 else:
4539 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004540 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004541
4542 change = cl.GetChange(base_branch, None)
4543 return owners_finder.OwnersFinder(
4544 [f.LocalPath() for f in
4545 cl.GetChange(base_branch, None).AffectedFiles()],
4546 change.RepositoryRoot(), author,
4547 fopen=file, os_path=os.path, glob=glob.glob,
4548 disable_color=options.no_color).run()
4549
4550
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004551def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004552 """Generates a diff command."""
4553 # Generate diff for the current branch's changes.
4554 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4555 upstream_commit, '--' ]
4556
4557 if args:
4558 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004559 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004560 diff_cmd.append(arg)
4561 else:
4562 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004563
4564 return diff_cmd
4565
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004566def MatchingFileType(file_name, extensions):
4567 """Returns true if the file name ends with one of the given extensions."""
4568 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004569
enne@chromium.org555cfe42014-01-29 18:21:39 +00004570@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004571def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004572 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00004573 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004574 GN_EXTS = ['.gn', '.gni']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004575 parser.add_option('--full', action='store_true',
4576 help='Reformat the full content of all touched files')
4577 parser.add_option('--dry-run', action='store_true',
4578 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004579 parser.add_option('--python', action='store_true',
4580 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004581 parser.add_option('--diff', action='store_true',
4582 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004583 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004584
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004585 # git diff generates paths against the root of the repository. Change
4586 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004587 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004588 if rel_base_path:
4589 os.chdir(rel_base_path)
4590
digit@chromium.org29e47272013-05-17 17:01:46 +00004591 # Grab the merge-base commit, i.e. the upstream commit of the current
4592 # branch when it was created or the last time it was rebased. This is
4593 # to cover the case where the user may have called "git fetch origin",
4594 # moving the origin branch to a newer commit, but hasn't rebased yet.
4595 upstream_commit = None
4596 cl = Changelist()
4597 upstream_branch = cl.GetUpstreamBranch()
4598 if upstream_branch:
4599 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4600 upstream_commit = upstream_commit.strip()
4601
4602 if not upstream_commit:
4603 DieWithError('Could not find base commit for this branch. '
4604 'Are you in detached state?')
4605
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004606 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4607 diff_output = RunGit(changed_files_cmd)
4608 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004609 # Filter out files deleted by this CL
4610 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004611
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004612 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4613 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4614 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004615 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004616
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004617 top_dir = os.path.normpath(
4618 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
4619
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004620 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4621 # formatted. This is used to block during the presubmit.
4622 return_value = 0
4623
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004624 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004625 # Locate the clang-format binary in the checkout
4626 try:
4627 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
4628 except clang_format.NotFoundError, e:
4629 DieWithError(e)
4630
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004631 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004632 cmd = [clang_format_tool]
4633 if not opts.dry_run and not opts.diff:
4634 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004635 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004636 if opts.diff:
4637 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004638 else:
4639 env = os.environ.copy()
4640 env['PATH'] = str(os.path.dirname(clang_format_tool))
4641 try:
4642 script = clang_format.FindClangFormatScriptInChromiumTree(
4643 'clang-format-diff.py')
4644 except clang_format.NotFoundError, e:
4645 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004646
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004647 cmd = [sys.executable, script, '-p0']
4648 if not opts.dry_run and not opts.diff:
4649 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004650
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004651 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
4652 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004653
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004654 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
4655 if opts.diff:
4656 sys.stdout.write(stdout)
4657 if opts.dry_run and len(stdout) > 0:
4658 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004659
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004660 # Similar code to above, but using yapf on .py files rather than clang-format
4661 # on C/C++ files
4662 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004663 yapf_tool = gclient_utils.FindExecutable('yapf')
4664 if yapf_tool is None:
4665 DieWithError('yapf not found in PATH')
4666
4667 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004668 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004669 cmd = [yapf_tool]
4670 if not opts.dry_run and not opts.diff:
4671 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004672 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004673 if opts.diff:
4674 sys.stdout.write(stdout)
4675 else:
4676 # TODO(sbc): yapf --lines mode still has some issues.
4677 # https://github.com/google/yapf/issues/154
4678 DieWithError('--python currently only works with --full')
4679
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004680 # Dart's formatter does not have the nice property of only operating on
4681 # modified chunks, so hard code full.
4682 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004683 try:
4684 command = [dart_format.FindDartFmtToolInChromiumTree()]
4685 if not opts.dry_run and not opts.diff:
4686 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004687 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004688
ppi@chromium.org6593d932016-03-03 15:41:15 +00004689 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004690 if opts.dry_run and stdout:
4691 return_value = 2
4692 except dart_format.NotFoundError as e:
erikcorry@chromium.org3e445022015-12-17 09:07:26 +00004693 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
4694 'found in this checkout. Files in other languages are still ' +
4695 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004696
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004697 # Format GN build files. Always run on full build files for canonical form.
4698 if gn_diff_files:
4699 cmd = ['gn', 'format']
4700 if not opts.dry_run and not opts.diff:
4701 cmd.append('--in-place')
4702 for gn_diff_file in gn_diff_files:
4703 stdout = RunCommand(cmd + [gn_diff_file], cwd=top_dir)
4704 if opts.diff:
4705 sys.stdout.write(stdout)
4706
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004707 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004708
4709
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004710@subcommand.usage('<codereview url or issue id>')
4711def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004712 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004713 _, args = parser.parse_args(args)
4714
4715 if len(args) != 1:
4716 parser.print_help()
4717 return 1
4718
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004719 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00004720 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004721 parser.print_help()
4722 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00004723 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004724
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004725 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00004726 output = RunGit(['config', '--local', '--get-regexp',
4727 r'branch\..*\.%s' % issueprefix],
4728 error_ok=True)
4729 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004730 if issue == target_issue:
4731 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004732
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004733 branches = []
4734 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00004735 branches.extend(find_issues(cls.IssueSettingSuffix()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004736 if len(branches) == 0:
4737 print 'No branch found for issue %s.' % target_issue
4738 return 1
4739 if len(branches) == 1:
4740 RunGit(['checkout', branches[0]])
4741 else:
4742 print 'Multiple branches match issue %s:' % target_issue
4743 for i in range(len(branches)):
4744 print '%d: %s' % (i, branches[i])
4745 which = raw_input('Choose by index: ')
4746 try:
4747 RunGit(['checkout', branches[int(which)]])
4748 except (IndexError, ValueError):
4749 print 'Invalid selection, not checking out any branch.'
4750 return 1
4751
4752 return 0
4753
4754
maruel@chromium.org29404b52014-09-08 22:58:00 +00004755def CMDlol(parser, args):
4756 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00004757 print zlib.decompress(base64.b64decode(
4758 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
4759 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
4760 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
4761 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00004762 return 0
4763
4764
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004765class OptionParser(optparse.OptionParser):
4766 """Creates the option parse and add --verbose support."""
4767 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004768 optparse.OptionParser.__init__(
4769 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004770 self.add_option(
4771 '-v', '--verbose', action='count', default=0,
4772 help='Use 2 times for more debugging info')
4773
4774 def parse_args(self, args=None, values=None):
4775 options, args = optparse.OptionParser.parse_args(self, args, values)
4776 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
4777 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
4778 return options, args
4779
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004780
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004781def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00004782 if sys.hexversion < 0x02060000:
4783 print >> sys.stderr, (
4784 '\nYour python version %s is unsupported, please upgrade.\n' %
4785 sys.version.split(' ', 1)[0])
4786 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004787
maruel@chromium.orgddd59412011-11-30 14:20:38 +00004788 # Reload settings.
4789 global settings
4790 settings = Settings()
4791
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004792 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004793 dispatcher = subcommand.CommandDispatcher(__name__)
4794 try:
4795 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00004796 except auth.AuthenticationError as e:
4797 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004798 except urllib2.HTTPError, e:
4799 if e.code != 500:
4800 raise
4801 DieWithError(
4802 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
4803 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00004804 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004805
4806
4807if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004808 # These affect sys.stdout so do it outside of main() to simplify mocks in
4809 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00004810 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004811 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00004812 try:
4813 sys.exit(main(sys.argv[1:]))
4814 except KeyboardInterrupt:
4815 sys.stderr.write('interrupted\n')
4816 sys.exit(1)