blob: 63d01adadd8a85b2047a6ab1f6156046e3d9e6e8 [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.orgf86c7d32016-04-01 19:27:30 +0000838class _ParsedIssueNumberArgument(object):
839 def __init__(self, issue=None, patchset=None, hostname=None):
840 self.issue = issue
841 self.patchset = patchset
842 self.hostname = hostname
843
844 @property
845 def valid(self):
846 return self.issue is not None
847
848
849class _RietveldParsedIssueNumberArgument(_ParsedIssueNumberArgument):
850 def __init__(self, *args, **kwargs):
851 self.patch_url = kwargs.pop('patch_url', None)
852 super(_RietveldParsedIssueNumberArgument, self).__init__(*args, **kwargs)
853
854
855def ParseIssueNumberArgument(arg):
856 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
857 fail_result = _ParsedIssueNumberArgument()
858
859 if arg.isdigit():
860 return _ParsedIssueNumberArgument(issue=int(arg))
861 if not arg.startswith('http'):
862 return fail_result
863 url = gclient_utils.UpgradeToHttps(arg)
864 try:
865 parsed_url = urlparse.urlparse(url)
866 except ValueError:
867 return fail_result
868 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
869 tmp = cls.ParseIssueURL(parsed_url)
870 if tmp is not None:
871 return tmp
872 return fail_result
873
874
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000875class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000876 """Changelist works with one changelist in local branch.
877
878 Supports two codereview backends: Rietveld or Gerrit, selected at object
879 creation.
880
881 Not safe for concurrent multi-{thread,process} use.
882 """
883
884 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
885 """Create a new ChangeList instance.
886
887 If issue is given, the codereview must be given too.
888
889 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
890 Otherwise, it's decided based on current configuration of the local branch,
891 with default being 'rietveld' for backwards compatibility.
892 See _load_codereview_impl for more details.
893
894 **kwargs will be passed directly to codereview implementation.
895 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000896 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000897 global settings
898 if not settings:
899 # Happens when git_cl.py is used as a utility library.
900 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000901
902 if issue:
903 assert codereview, 'codereview must be known, if issue is known'
904
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000905 self.branchref = branchref
906 if self.branchref:
907 self.branch = ShortBranchName(self.branchref)
908 else:
909 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000910 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000911 self.lookedup_issue = False
912 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000913 self.has_description = False
914 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000915 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000916 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000917 self.cc = None
918 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000919 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000920
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000921 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000922 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000923 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000924 assert self._codereview_impl
925 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000926
927 def _load_codereview_impl(self, codereview=None, **kwargs):
928 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000929 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
930 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
931 self._codereview = codereview
932 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000933 return
934
935 # Automatic selection based on issue number set for a current branch.
936 # Rietveld takes precedence over Gerrit.
937 assert not self.issue
938 # Whether we find issue or not, we are doing the lookup.
939 self.lookedup_issue = True
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000940 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000941 setting = cls.IssueSetting(self.GetBranch())
942 issue = RunGit(['config', setting], error_ok=True).strip()
943 if issue:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000944 self._codereview = codereview
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000945 self._codereview_impl = cls(self, **kwargs)
946 self.issue = int(issue)
947 return
948
949 # No issue is set for this branch, so decide based on repo-wide settings.
950 return self._load_codereview_impl(
951 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
952 **kwargs)
953
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000954 def IsGerrit(self):
955 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000956
957 def GetCCList(self):
958 """Return the users cc'd on this CL.
959
960 Return is a string suitable for passing to gcl with the --cc flag.
961 """
962 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000963 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000964 more_cc = ','.join(self.watchers)
965 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
966 return self.cc
967
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000968 def GetCCListWithoutDefault(self):
969 """Return the users cc'd on this CL excluding default ones."""
970 if self.cc is None:
971 self.cc = ','.join(self.watchers)
972 return self.cc
973
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000974 def SetWatchers(self, watchers):
975 """Set the list of email addresses that should be cc'd based on the changed
976 files in this CL.
977 """
978 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000979
980 def GetBranch(self):
981 """Returns the short branch name, e.g. 'master'."""
982 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000983 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000984 if not branchref:
985 return None
986 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000987 self.branch = ShortBranchName(self.branchref)
988 return self.branch
989
990 def GetBranchRef(self):
991 """Returns the full branch name, e.g. 'refs/heads/master'."""
992 self.GetBranch() # Poke the lazy loader.
993 return self.branchref
994
tandrii@chromium.org534f67a2016-04-07 18:47:05 +0000995 def ClearBranch(self):
996 """Clears cached branch data of this object."""
997 self.branch = self.branchref = None
998
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000999 @staticmethod
1000 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001001 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001002 e.g. 'origin', 'refs/heads/master'
1003 """
1004 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001005 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
1006 error_ok=True).strip()
1007 if upstream_branch:
1008 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
1009 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001010 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1011 error_ok=True).strip()
1012 if upstream_branch:
1013 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001014 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001015 # Fall back on trying a git-svn upstream branch.
1016 if settings.GetIsGitSvn():
1017 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001018 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001019 # Else, try to guess the origin remote.
1020 remote_branches = RunGit(['branch', '-r']).split()
1021 if 'origin/master' in remote_branches:
1022 # Fall back on origin/master if it exits.
1023 remote = 'origin'
1024 upstream_branch = 'refs/heads/master'
1025 elif 'origin/trunk' in remote_branches:
1026 # Fall back on origin/trunk if it exists. Generally a shared
1027 # git-svn clone
1028 remote = 'origin'
1029 upstream_branch = 'refs/heads/trunk'
1030 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001031 DieWithError(
1032 'Unable to determine default branch to diff against.\n'
1033 'Either pass complete "git diff"-style arguments, like\n'
1034 ' git cl upload origin/master\n'
1035 'or verify this branch is set up to track another \n'
1036 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001037
1038 return remote, upstream_branch
1039
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001040 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001041 upstream_branch = self.GetUpstreamBranch()
1042 if not BranchExists(upstream_branch):
1043 DieWithError('The upstream for the current branch (%s) does not exist '
1044 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001045 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001046 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001047
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001048 def GetUpstreamBranch(self):
1049 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001050 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001051 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001052 upstream_branch = upstream_branch.replace('refs/heads/',
1053 'refs/remotes/%s/' % remote)
1054 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1055 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001056 self.upstream_branch = upstream_branch
1057 return self.upstream_branch
1058
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001059 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001060 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001061 remote, branch = None, self.GetBranch()
1062 seen_branches = set()
1063 while branch not in seen_branches:
1064 seen_branches.add(branch)
1065 remote, branch = self.FetchUpstreamTuple(branch)
1066 branch = ShortBranchName(branch)
1067 if remote != '.' or branch.startswith('refs/remotes'):
1068 break
1069 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001070 remotes = RunGit(['remote'], error_ok=True).split()
1071 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001072 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001073 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001074 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001075 logging.warning('Could not determine which remote this change is '
1076 'associated with, so defaulting to "%s". This may '
1077 'not be what you want. You may prevent this message '
1078 'by running "git svn info" as documented here: %s',
1079 self._remote,
1080 GIT_INSTRUCTIONS_URL)
1081 else:
1082 logging.warn('Could not determine which remote this change is '
1083 'associated with. You may prevent this message by '
1084 'running "git svn info" as documented here: %s',
1085 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001086 branch = 'HEAD'
1087 if branch.startswith('refs/remotes'):
1088 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001089 elif branch.startswith('refs/branch-heads/'):
1090 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001091 else:
1092 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001093 return self._remote
1094
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001095 def GitSanityChecks(self, upstream_git_obj):
1096 """Checks git repo status and ensures diff is from local commits."""
1097
sbc@chromium.org79706062015-01-14 21:18:12 +00001098 if upstream_git_obj is None:
1099 if self.GetBranch() is None:
1100 print >> sys.stderr, (
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00001101 'ERROR: unable to determine current branch (detached HEAD?)')
sbc@chromium.org79706062015-01-14 21:18:12 +00001102 else:
1103 print >> sys.stderr, (
1104 'ERROR: no upstream branch')
1105 return False
1106
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001107 # Verify the commit we're diffing against is in our current branch.
1108 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1109 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1110 if upstream_sha != common_ancestor:
1111 print >> sys.stderr, (
1112 'ERROR: %s is not in the current branch. You may need to rebase '
1113 'your tracking branch' % upstream_sha)
1114 return False
1115
1116 # List the commits inside the diff, and verify they are all local.
1117 commits_in_diff = RunGit(
1118 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1119 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1120 remote_branch = remote_branch.strip()
1121 if code != 0:
1122 _, remote_branch = self.GetRemoteBranch()
1123
1124 commits_in_remote = RunGit(
1125 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1126
1127 common_commits = set(commits_in_diff) & set(commits_in_remote)
1128 if common_commits:
1129 print >> sys.stderr, (
1130 'ERROR: Your diff contains %d commits already in %s.\n'
1131 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1132 'the diff. If you are using a custom git flow, you can override'
1133 ' the reference used for this check with "git config '
1134 'gitcl.remotebranch <git-ref>".' % (
1135 len(common_commits), remote_branch, upstream_git_obj))
1136 return False
1137 return True
1138
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001139 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001140 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001141
1142 Returns None if it is not set.
1143 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001144 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
1145 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001146
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001147 def GetGitSvnRemoteUrl(self):
1148 """Return the configured git-svn remote URL parsed from git svn info.
1149
1150 Returns None if it is not set.
1151 """
1152 # URL is dependent on the current directory.
1153 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1154 if data:
1155 keys = dict(line.split(': ', 1) for line in data.splitlines()
1156 if ': ' in line)
1157 return keys.get('URL', None)
1158 return None
1159
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001160 def GetRemoteUrl(self):
1161 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1162
1163 Returns None if there is no remote.
1164 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001165 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001166 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1167
1168 # If URL is pointing to a local directory, it is probably a git cache.
1169 if os.path.isdir(url):
1170 url = RunGit(['config', 'remote.%s.url' % remote],
1171 error_ok=True,
1172 cwd=url).strip()
1173 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001174
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001175 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001176 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001177 if self.issue is None and not self.lookedup_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001178 issue = RunGit(['config',
1179 self._codereview_impl.IssueSetting(self.GetBranch())],
1180 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001181 self.issue = int(issue) or None if issue else None
1182 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001183 return self.issue
1184
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001185 def GetIssueURL(self):
1186 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001187 issue = self.GetIssue()
1188 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001189 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001190 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001191
1192 def GetDescription(self, pretty=False):
1193 if not self.has_description:
1194 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001195 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001196 self.has_description = True
1197 if pretty:
1198 wrapper = textwrap.TextWrapper()
1199 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1200 return wrapper.fill(self.description)
1201 return self.description
1202
1203 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001204 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001205 if self.patchset is None and not self.lookedup_patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001206 patchset = RunGit(['config', self._codereview_impl.PatchsetSetting()],
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001207 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001208 self.patchset = int(patchset) or None if patchset else None
1209 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001210 return self.patchset
1211
1212 def SetPatchset(self, patchset):
1213 """Set this branch's patchset. If patchset=0, clears the patchset."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001214 patchset_setting = self._codereview_impl.PatchsetSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001215 if patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001216 RunGit(['config', patchset_setting, str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001217 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001218 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001219 RunGit(['config', '--unset', patchset_setting],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001220 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001221 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001222
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001223 def SetIssue(self, issue=None):
1224 """Set this branch's issue. If issue isn't given, clears the issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001225 issue_setting = self._codereview_impl.IssueSetting(self.GetBranch())
1226 codereview_setting = self._codereview_impl.GetCodereviewServerSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001227 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001228 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001229 RunGit(['config', issue_setting, str(issue)])
1230 codereview_server = self._codereview_impl.GetCodereviewServer()
1231 if codereview_server:
1232 RunGit(['config', codereview_setting, codereview_server])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001233 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +00001234 current_issue = self.GetIssue()
1235 if current_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001236 RunGit(['config', '--unset', issue_setting])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001237 self.issue = None
1238 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001239
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001240 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001241 if not self.GitSanityChecks(upstream_branch):
1242 DieWithError('\nGit sanity check failure')
1243
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001244 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001245 if not root:
1246 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001247 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001248
1249 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001250 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001251 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001252 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001253 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001254 except subprocess2.CalledProcessError:
1255 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001256 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001257 'This branch probably doesn\'t exist anymore. To reset the\n'
1258 'tracking branch, please run\n'
1259 ' git branch --set-upstream %s trunk\n'
1260 'replacing trunk with origin/master or the relevant branch') %
1261 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001262
maruel@chromium.org52424302012-08-29 15:14:30 +00001263 issue = self.GetIssue()
1264 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001265 if issue:
1266 description = self.GetDescription()
1267 else:
1268 # If the change was never uploaded, use the log messages of all commits
1269 # up to the branch point, as git cl upload will prefill the description
1270 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001271 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1272 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001273
1274 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001275 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001276 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001277 name,
1278 description,
1279 absroot,
1280 files,
1281 issue,
1282 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001283 author,
1284 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001285
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001286 def UpdateDescription(self, description):
1287 self.description = description
1288 return self._codereview_impl.UpdateDescriptionRemote(description)
1289
1290 def RunHook(self, committing, may_prompt, verbose, change):
1291 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1292 try:
1293 return presubmit_support.DoPresubmitChecks(change, committing,
1294 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1295 default_presubmit=None, may_prompt=may_prompt,
1296 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit())
1297 except presubmit_support.PresubmitFailure, e:
1298 DieWithError(
1299 ('%s\nMaybe your depot_tools is out of date?\n'
1300 'If all fails, contact maruel@') % e)
1301
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001302 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1303 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001304 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1305 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001306 else:
1307 # Assume url.
1308 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1309 urlparse.urlparse(issue_arg))
1310 if not parsed_issue_arg or not parsed_issue_arg.valid:
1311 DieWithError('Failed to parse issue argument "%s". '
1312 'Must be an issue number or a valid URL.' % issue_arg)
1313 return self._codereview_impl.CMDPatchWithParsedIssue(
1314 parsed_issue_arg, reject, nocommit, directory)
1315
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001316 def CMDUpload(self, options, git_diff_args, orig_args):
1317 """Uploads a change to codereview."""
1318 if git_diff_args:
1319 # TODO(ukai): is it ok for gerrit case?
1320 base_branch = git_diff_args[0]
1321 else:
1322 if self.GetBranch() is None:
1323 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1324
1325 # Default to diffing against common ancestor of upstream branch
1326 base_branch = self.GetCommonAncestorWithUpstream()
1327 git_diff_args = [base_branch, 'HEAD']
1328
1329 # Make sure authenticated to codereview before running potentially expensive
1330 # hooks. It is a fast, best efforts check. Codereview still can reject the
1331 # authentication during the actual upload.
1332 self._codereview_impl.EnsureAuthenticated()
1333
1334 # Apply watchlists on upload.
1335 change = self.GetChange(base_branch, None)
1336 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1337 files = [f.LocalPath() for f in change.AffectedFiles()]
1338 if not options.bypass_watchlists:
1339 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1340
1341 if not options.bypass_hooks:
1342 if options.reviewers or options.tbr_owners:
1343 # Set the reviewer list now so that presubmit checks can access it.
1344 change_description = ChangeDescription(change.FullDescriptionText())
1345 change_description.update_reviewers(options.reviewers,
1346 options.tbr_owners,
1347 change)
1348 change.SetDescriptionText(change_description.description)
1349 hook_results = self.RunHook(committing=False,
1350 may_prompt=not options.force,
1351 verbose=options.verbose,
1352 change=change)
1353 if not hook_results.should_continue():
1354 return 1
1355 if not options.reviewers and hook_results.reviewers:
1356 options.reviewers = hook_results.reviewers.split(',')
1357
1358 if self.GetIssue():
1359 latest_patchset = self.GetMostRecentPatchset()
1360 local_patchset = self.GetPatchset()
1361 if (latest_patchset and local_patchset and
1362 local_patchset != latest_patchset):
1363 print ('The last upload made from this repository was patchset #%d but '
1364 'the most recent patchset on the server is #%d.'
1365 % (local_patchset, latest_patchset))
1366 print ('Uploading will still work, but if you\'ve uploaded to this '
1367 'issue from another machine or branch the patch you\'re '
1368 'uploading now might not include those changes.')
1369 ask_for_data('About to upload; enter to confirm.')
1370
1371 print_stats(options.similarity, options.find_copies, git_diff_args)
1372 ret = self.CMDUploadChange(options, git_diff_args, change)
1373 if not ret:
1374 git_set_branch_value('last-upload-hash',
1375 RunGit(['rev-parse', 'HEAD']).strip())
1376 # Run post upload hooks, if specified.
1377 if settings.GetRunPostUploadHook():
1378 presubmit_support.DoPostUploadExecuter(
1379 change,
1380 self,
1381 settings.GetRoot(),
1382 options.verbose,
1383 sys.stdout)
1384
1385 # Upload all dependencies if specified.
1386 if options.dependencies:
1387 print
1388 print '--dependencies has been specified.'
1389 print 'All dependent local branches will be re-uploaded.'
1390 print
1391 # Remove the dependencies flag from args so that we do not end up in a
1392 # loop.
1393 orig_args.remove('--dependencies')
1394 ret = upload_branch_deps(self, orig_args)
1395 return ret
1396
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001397 # Forward methods to codereview specific implementation.
1398
1399 def CloseIssue(self):
1400 return self._codereview_impl.CloseIssue()
1401
1402 def GetStatus(self):
1403 return self._codereview_impl.GetStatus()
1404
1405 def GetCodereviewServer(self):
1406 return self._codereview_impl.GetCodereviewServer()
1407
1408 def GetApprovingReviewers(self):
1409 return self._codereview_impl.GetApprovingReviewers()
1410
1411 def GetMostRecentPatchset(self):
1412 return self._codereview_impl.GetMostRecentPatchset()
1413
1414 def __getattr__(self, attr):
1415 # This is because lots of untested code accesses Rietveld-specific stuff
1416 # directly, and it's hard to fix for sure. So, just let it work, and fix
1417 # on a cases by case basis.
1418 return getattr(self._codereview_impl, attr)
1419
1420
1421class _ChangelistCodereviewBase(object):
1422 """Abstract base class encapsulating codereview specifics of a changelist."""
1423 def __init__(self, changelist):
1424 self._changelist = changelist # instance of Changelist
1425
1426 def __getattr__(self, attr):
1427 # Forward methods to changelist.
1428 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1429 # _RietveldChangelistImpl to avoid this hack?
1430 return getattr(self._changelist, attr)
1431
1432 def GetStatus(self):
1433 """Apply a rough heuristic to give a simple summary of an issue's review
1434 or CQ status, assuming adherence to a common workflow.
1435
1436 Returns None if no issue for this branch, or specific string keywords.
1437 """
1438 raise NotImplementedError()
1439
1440 def GetCodereviewServer(self):
1441 """Returns server URL without end slash, like "https://codereview.com"."""
1442 raise NotImplementedError()
1443
1444 def FetchDescription(self):
1445 """Fetches and returns description from the codereview server."""
1446 raise NotImplementedError()
1447
1448 def GetCodereviewServerSetting(self):
1449 """Returns git config setting for the codereview server."""
1450 raise NotImplementedError()
1451
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001452 @classmethod
1453 def IssueSetting(cls, branch):
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001454 return 'branch.%s.%s' % (branch, cls.IssueSettingSuffix())
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001455
1456 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001457 def IssueSettingSuffix(cls):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001458 """Returns name of git config setting which stores issue number for a given
1459 branch."""
1460 raise NotImplementedError()
1461
1462 def PatchsetSetting(self):
1463 """Returns name of git config setting which stores issue number."""
1464 raise NotImplementedError()
1465
1466 def GetRieveldObjForPresubmit(self):
1467 # This is an unfortunate Rietveld-embeddedness in presubmit.
1468 # For non-Rietveld codereviews, this probably should return a dummy object.
1469 raise NotImplementedError()
1470
1471 def UpdateDescriptionRemote(self, description):
1472 """Update the description on codereview site."""
1473 raise NotImplementedError()
1474
1475 def CloseIssue(self):
1476 """Closes the issue."""
1477 raise NotImplementedError()
1478
1479 def GetApprovingReviewers(self):
1480 """Returns a list of reviewers approving the change.
1481
1482 Note: not necessarily committers.
1483 """
1484 raise NotImplementedError()
1485
1486 def GetMostRecentPatchset(self):
1487 """Returns the most recent patchset number from the codereview site."""
1488 raise NotImplementedError()
1489
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001490 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1491 directory):
1492 """Fetches and applies the issue.
1493
1494 Arguments:
1495 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1496 reject: if True, reject the failed patch instead of switching to 3-way
1497 merge. Rietveld only.
1498 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1499 only.
1500 directory: switch to directory before applying the patch. Rietveld only.
1501 """
1502 raise NotImplementedError()
1503
1504 @staticmethod
1505 def ParseIssueURL(parsed_url):
1506 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1507 failed."""
1508 raise NotImplementedError()
1509
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001510 def EnsureAuthenticated(self):
1511 """Best effort check that user is authenticated with codereview server."""
1512 raise NotImplementedError()
1513
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001514 def CMDUploadChange(self, options, args, change):
1515 """Uploads a change to codereview."""
1516 raise NotImplementedError()
1517
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001518
1519class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1520 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1521 super(_RietveldChangelistImpl, self).__init__(changelist)
1522 assert settings, 'must be initialized in _ChangelistCodereviewBase'
1523 settings.GetDefaultServerUrl()
1524
1525 self._rietveld_server = rietveld_server
1526 self._auth_config = auth_config
1527 self._props = None
1528 self._rpc_server = None
1529
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001530 def GetCodereviewServer(self):
1531 if not self._rietveld_server:
1532 # If we're on a branch then get the server potentially associated
1533 # with that branch.
1534 if self.GetIssue():
1535 rietveld_server_setting = self.GetCodereviewServerSetting()
1536 if rietveld_server_setting:
1537 self._rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1538 ['config', rietveld_server_setting], error_ok=True).strip())
1539 if not self._rietveld_server:
1540 self._rietveld_server = settings.GetDefaultServerUrl()
1541 return self._rietveld_server
1542
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001543 def EnsureAuthenticated(self):
1544 """Best effort check that user is authenticated with Rietveld server."""
1545 if self._auth_config.use_oauth2:
1546 authenticator = auth.get_authenticator_for_host(
1547 self.GetCodereviewServer(), self._auth_config)
1548 if not authenticator.has_cached_credentials():
1549 raise auth.LoginRequiredError(self.GetCodereviewServer())
1550
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001551 def FetchDescription(self):
1552 issue = self.GetIssue()
1553 assert issue
1554 try:
1555 return self.RpcServer().get_description(issue).strip()
1556 except urllib2.HTTPError as e:
1557 if e.code == 404:
1558 DieWithError(
1559 ('\nWhile fetching the description for issue %d, received a '
1560 '404 (not found)\n'
1561 'error. It is likely that you deleted this '
1562 'issue on the server. If this is the\n'
1563 'case, please run\n\n'
1564 ' git cl issue 0\n\n'
1565 'to clear the association with the deleted issue. Then run '
1566 'this command again.') % issue)
1567 else:
1568 DieWithError(
1569 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1570 except urllib2.URLError as e:
1571 print >> sys.stderr, (
1572 'Warning: Failed to retrieve CL description due to network '
1573 'failure.')
1574 return ''
1575
1576 def GetMostRecentPatchset(self):
1577 return self.GetIssueProperties()['patchsets'][-1]
1578
1579 def GetPatchSetDiff(self, issue, patchset):
1580 return self.RpcServer().get(
1581 '/download/issue%s_%s.diff' % (issue, patchset))
1582
1583 def GetIssueProperties(self):
1584 if self._props is None:
1585 issue = self.GetIssue()
1586 if not issue:
1587 self._props = {}
1588 else:
1589 self._props = self.RpcServer().get_issue_properties(issue, True)
1590 return self._props
1591
1592 def GetApprovingReviewers(self):
1593 return get_approving_reviewers(self.GetIssueProperties())
1594
1595 def AddComment(self, message):
1596 return self.RpcServer().add_comment(self.GetIssue(), message)
1597
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001598 def GetStatus(self):
1599 """Apply a rough heuristic to give a simple summary of an issue's review
1600 or CQ status, assuming adherence to a common workflow.
1601
1602 Returns None if no issue for this branch, or one of the following keywords:
1603 * 'error' - error from review tool (including deleted issues)
1604 * 'unsent' - not sent for review
1605 * 'waiting' - waiting for review
1606 * 'reply' - waiting for owner to reply to review
1607 * 'lgtm' - LGTM from at least one approved reviewer
1608 * 'commit' - in the commit queue
1609 * 'closed' - closed
1610 """
1611 if not self.GetIssue():
1612 return None
1613
1614 try:
1615 props = self.GetIssueProperties()
1616 except urllib2.HTTPError:
1617 return 'error'
1618
1619 if props.get('closed'):
1620 # Issue is closed.
1621 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001622 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001623 # Issue is in the commit queue.
1624 return 'commit'
1625
1626 try:
1627 reviewers = self.GetApprovingReviewers()
1628 except urllib2.HTTPError:
1629 return 'error'
1630
1631 if reviewers:
1632 # Was LGTM'ed.
1633 return 'lgtm'
1634
1635 messages = props.get('messages') or []
1636
1637 if not messages:
1638 # No message was sent.
1639 return 'unsent'
1640 if messages[-1]['sender'] != props.get('owner_email'):
1641 # Non-LGTM reply from non-owner
1642 return 'reply'
1643 return 'waiting'
1644
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001645 def UpdateDescriptionRemote(self, description):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001646 return self.RpcServer().update_description(
1647 self.GetIssue(), self.description)
1648
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001649 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001650 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001651
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001652 def SetFlag(self, flag, value):
1653 """Patchset must match."""
1654 if not self.GetPatchset():
1655 DieWithError('The patchset needs to match. Send another patchset.')
1656 try:
1657 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001658 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001659 except urllib2.HTTPError, e:
1660 if e.code == 404:
1661 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1662 if e.code == 403:
1663 DieWithError(
1664 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1665 'match?') % (self.GetIssue(), self.GetPatchset()))
1666 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001667
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001668 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001669 """Returns an upload.RpcServer() to access this review's rietveld instance.
1670 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001671 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001672 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001673 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001674 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001675 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001676
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001677 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001678 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001679 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001680
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001681 def PatchsetSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001682 """Return the git setting that stores this change's most recent patchset."""
1683 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1684
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001685 def GetCodereviewServerSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001686 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001687 branch = self.GetBranch()
1688 if branch:
1689 return 'branch.%s.rietveldserver' % branch
1690 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001691
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001692 def GetRieveldObjForPresubmit(self):
1693 return self.RpcServer()
1694
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001695 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1696 directory):
1697 # TODO(maruel): Use apply_issue.py
1698
1699 # PatchIssue should never be called with a dirty tree. It is up to the
1700 # caller to check this, but just in case we assert here since the
1701 # consequences of the caller not checking this could be dire.
1702 assert(not git_common.is_dirty_git_tree('apply'))
1703 assert(parsed_issue_arg.valid)
1704 self._changelist.issue = parsed_issue_arg.issue
1705 if parsed_issue_arg.hostname:
1706 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1707
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001708 if (isinstance(parsed_issue_arg, _RietveldParsedIssueNumberArgument) and
1709 parsed_issue_arg.patch_url):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001710 assert parsed_issue_arg.patchset
1711 patchset = parsed_issue_arg.patchset
1712 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1713 else:
1714 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1715 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1716
1717 # Switch up to the top-level directory, if necessary, in preparation for
1718 # applying the patch.
1719 top = settings.GetRelativeRoot()
1720 if top:
1721 os.chdir(top)
1722
1723 # Git patches have a/ at the beginning of source paths. We strip that out
1724 # with a sed script rather than the -p flag to patch so we can feed either
1725 # Git or svn-style patches into the same apply command.
1726 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1727 try:
1728 patch_data = subprocess2.check_output(
1729 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1730 except subprocess2.CalledProcessError:
1731 DieWithError('Git patch mungling failed.')
1732 logging.info(patch_data)
1733
1734 # We use "git apply" to apply the patch instead of "patch" so that we can
1735 # pick up file adds.
1736 # The --index flag means: also insert into the index (so we catch adds).
1737 cmd = ['git', 'apply', '--index', '-p0']
1738 if directory:
1739 cmd.extend(('--directory', directory))
1740 if reject:
1741 cmd.append('--reject')
1742 elif IsGitVersionAtLeast('1.7.12'):
1743 cmd.append('--3way')
1744 try:
1745 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
1746 stdin=patch_data, stdout=subprocess2.VOID)
1747 except subprocess2.CalledProcessError:
1748 print 'Failed to apply the patch'
1749 return 1
1750
1751 # If we had an issue, commit the current state and register the issue.
1752 if not nocommit:
1753 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
1754 'patch from issue %(i)s at patchset '
1755 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
1756 % {'i': self.GetIssue(), 'p': patchset})])
1757 self.SetIssue(self.GetIssue())
1758 self.SetPatchset(patchset)
1759 print "Committed patch locally."
1760 else:
1761 print "Patch applied to index."
1762 return 0
1763
1764 @staticmethod
1765 def ParseIssueURL(parsed_url):
1766 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1767 return None
1768 # Typical url: https://domain/<issue_number>[/[other]]
1769 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
1770 if match:
1771 return _RietveldParsedIssueNumberArgument(
1772 issue=int(match.group(1)),
1773 hostname=parsed_url.netloc)
1774 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
1775 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
1776 if match:
1777 return _RietveldParsedIssueNumberArgument(
1778 issue=int(match.group(1)),
1779 patchset=int(match.group(2)),
1780 hostname=parsed_url.netloc,
1781 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
1782 return None
1783
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001784 def CMDUploadChange(self, options, args, change):
1785 """Upload the patch to Rietveld."""
1786 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1787 upload_args.extend(['--server', self.GetCodereviewServer()])
1788 # TODO(tandrii): refactor this ugliness into _RietveldChangelistImpl.
1789 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
1790 if options.emulate_svn_auto_props:
1791 upload_args.append('--emulate_svn_auto_props')
1792
1793 change_desc = None
1794
1795 if options.email is not None:
1796 upload_args.extend(['--email', options.email])
1797
1798 if self.GetIssue():
1799 if options.title:
1800 upload_args.extend(['--title', options.title])
1801 if options.message:
1802 upload_args.extend(['--message', options.message])
1803 upload_args.extend(['--issue', str(self.GetIssue())])
1804 print ('This branch is associated with issue %s. '
1805 'Adding patch to that issue.' % self.GetIssue())
1806 else:
1807 if options.title:
1808 upload_args.extend(['--title', options.title])
1809 message = (options.title or options.message or
1810 CreateDescriptionFromLog(args))
1811 change_desc = ChangeDescription(message)
1812 if options.reviewers or options.tbr_owners:
1813 change_desc.update_reviewers(options.reviewers,
1814 options.tbr_owners,
1815 change)
1816 if not options.force:
1817 change_desc.prompt()
1818
1819 if not change_desc.description:
1820 print "Description is empty; aborting."
1821 return 1
1822
1823 upload_args.extend(['--message', change_desc.description])
1824 if change_desc.get_reviewers():
1825 upload_args.append('--reviewers=%s' % ','.join(
1826 change_desc.get_reviewers()))
1827 if options.send_mail:
1828 if not change_desc.get_reviewers():
1829 DieWithError("Must specify reviewers to send email.")
1830 upload_args.append('--send_mail')
1831
1832 # We check this before applying rietveld.private assuming that in
1833 # rietveld.cc only addresses which we can send private CLs to are listed
1834 # if rietveld.private is set, and so we should ignore rietveld.cc only
1835 # when --private is specified explicitly on the command line.
1836 if options.private:
1837 logging.warn('rietveld.cc is ignored since private flag is specified. '
1838 'You need to review and add them manually if necessary.')
1839 cc = self.GetCCListWithoutDefault()
1840 else:
1841 cc = self.GetCCList()
1842 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
1843 if cc:
1844 upload_args.extend(['--cc', cc])
1845
1846 if options.private or settings.GetDefaultPrivateFlag() == "True":
1847 upload_args.append('--private')
1848
1849 upload_args.extend(['--git_similarity', str(options.similarity)])
1850 if not options.find_copies:
1851 upload_args.extend(['--git_no_find_copies'])
1852
1853 # Include the upstream repo's URL in the change -- this is useful for
1854 # projects that have their source spread across multiple repos.
1855 remote_url = self.GetGitBaseUrlFromConfig()
1856 if not remote_url:
1857 if settings.GetIsGitSvn():
1858 remote_url = self.GetGitSvnRemoteUrl()
1859 else:
1860 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
1861 remote_url = '%s@%s' % (self.GetRemoteUrl(),
1862 self.GetUpstreamBranch().split('/')[-1])
1863 if remote_url:
1864 upload_args.extend(['--base_url', remote_url])
1865 remote, remote_branch = self.GetRemoteBranch()
1866 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
1867 settings.GetPendingRefPrefix())
1868 if target_ref:
1869 upload_args.extend(['--target_ref', target_ref])
1870
1871 # Look for dependent patchsets. See crbug.com/480453 for more details.
1872 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
1873 upstream_branch = ShortBranchName(upstream_branch)
1874 if remote is '.':
1875 # A local branch is being tracked.
1876 local_branch = ShortBranchName(upstream_branch)
1877 if settings.GetIsSkipDependencyUpload(local_branch):
1878 print
1879 print ('Skipping dependency patchset upload because git config '
1880 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
1881 print
1882 else:
1883 auth_config = auth.extract_auth_config_from_options(options)
1884 branch_cl = Changelist(branchref=local_branch,
1885 auth_config=auth_config)
1886 branch_cl_issue_url = branch_cl.GetIssueURL()
1887 branch_cl_issue = branch_cl.GetIssue()
1888 branch_cl_patchset = branch_cl.GetPatchset()
1889 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
1890 upload_args.extend(
1891 ['--depends_on_patchset', '%s:%s' % (
1892 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001893 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001894 '\n'
1895 'The current branch (%s) is tracking a local branch (%s) with '
1896 'an associated CL.\n'
1897 'Adding %s/#ps%s as a dependency patchset.\n'
1898 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
1899 branch_cl_patchset))
1900
1901 project = settings.GetProject()
1902 if project:
1903 upload_args.extend(['--project', project])
1904
1905 if options.cq_dry_run:
1906 upload_args.extend(['--cq_dry_run'])
1907
1908 try:
1909 upload_args = ['upload'] + upload_args + args
1910 logging.info('upload.RealMain(%s)', upload_args)
1911 issue, patchset = upload.RealMain(upload_args)
1912 issue = int(issue)
1913 patchset = int(patchset)
1914 except KeyboardInterrupt:
1915 sys.exit(1)
1916 except:
1917 # If we got an exception after the user typed a description for their
1918 # change, back up the description before re-raising.
1919 if change_desc:
1920 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1921 print('\nGot exception while uploading -- saving description to %s\n' %
1922 backup_path)
1923 backup_file = open(backup_path, 'w')
1924 backup_file.write(change_desc.description)
1925 backup_file.close()
1926 raise
1927
1928 if not self.GetIssue():
1929 self.SetIssue(issue)
1930 self.SetPatchset(patchset)
1931
1932 if options.use_commit_queue:
1933 self.SetFlag('commit', '1')
1934 return 0
1935
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001936
1937class _GerritChangelistImpl(_ChangelistCodereviewBase):
1938 def __init__(self, changelist, auth_config=None):
1939 # auth_config is Rietveld thing, kept here to preserve interface only.
1940 super(_GerritChangelistImpl, self).__init__(changelist)
1941 self._change_id = None
1942 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
1943 self._gerrit_host = None # e.g. chromium-review.googlesource.com
1944
1945 def _GetGerritHost(self):
1946 # Lazy load of configs.
1947 self.GetCodereviewServer()
1948 return self._gerrit_host
1949
1950 def GetCodereviewServer(self):
1951 if not self._gerrit_server:
1952 # If we're on a branch then get the server potentially associated
1953 # with that branch.
1954 if self.GetIssue():
1955 gerrit_server_setting = self.GetCodereviewServerSetting()
1956 if gerrit_server_setting:
1957 self._gerrit_server = RunGit(['config', gerrit_server_setting],
1958 error_ok=True).strip()
1959 if self._gerrit_server:
1960 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
1961 if not self._gerrit_server:
1962 # We assume repo to be hosted on Gerrit, and hence Gerrit server
1963 # has "-review" suffix for lowest level subdomain.
1964 parts = urlparse.urlparse(self.GetRemoteUrl()).netloc.split('.')
1965 parts[0] = parts[0] + '-review'
1966 self._gerrit_host = '.'.join(parts)
1967 self._gerrit_server = 'https://%s' % self._gerrit_host
1968 return self._gerrit_server
1969
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001970 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001971 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001972 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001973
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001974 def EnsureAuthenticated(self):
1975 """Best effort check that user is authenticated with Gerrit server."""
1976 #TODO(tandrii): implement per bug http://crbug.com/583153.
1977
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001978 def PatchsetSetting(self):
1979 """Return the git setting that stores this change's most recent patchset."""
1980 return 'branch.%s.gerritpatchset' % self.GetBranch()
1981
1982 def GetCodereviewServerSetting(self):
1983 """Returns the git setting that stores this change's Gerrit server."""
1984 branch = self.GetBranch()
1985 if branch:
1986 return 'branch.%s.gerritserver' % branch
1987 return None
1988
1989 def GetRieveldObjForPresubmit(self):
1990 class ThisIsNotRietveldIssue(object):
1991 def __nonzero__(self):
1992 # This is a hack to make presubmit_support think that rietveld is not
1993 # defined, yet still ensure that calls directly result in a decent
1994 # exception message below.
1995 return False
1996
1997 def __getattr__(self, attr):
1998 print(
1999 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2000 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2001 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2002 'or use Rietveld for codereview.\n'
2003 'See also http://crbug.com/579160.' % attr)
2004 raise NotImplementedError()
2005 return ThisIsNotRietveldIssue()
2006
2007 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002008 """Apply a rough heuristic to give a simple summary of an issue's review
2009 or CQ status, assuming adherence to a common workflow.
2010
2011 Returns None if no issue for this branch, or one of the following keywords:
2012 * 'error' - error from review tool (including deleted issues)
2013 * 'unsent' - no reviewers added
2014 * 'waiting' - waiting for review
2015 * 'reply' - waiting for owner to reply to review
2016 * 'not lgtm' - Code-Review -2 from at least one approved reviewer
2017 * 'lgtm' - Code-Review +2 from at least one approved reviewer
2018 * 'commit' - in the commit queue
2019 * 'closed' - abandoned
2020 """
2021 if not self.GetIssue():
2022 return None
2023
2024 try:
2025 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
2026 except httplib.HTTPException:
2027 return 'error'
2028
2029 if data['status'] == 'ABANDONED':
2030 return 'closed'
2031
2032 cq_label = data['labels'].get('Commit-Queue', {})
2033 if cq_label:
2034 # Vote value is a stringified integer, which we expect from 0 to 2.
2035 vote_value = cq_label.get('value', '0')
2036 vote_text = cq_label.get('values', {}).get(vote_value, '')
2037 if vote_text.lower() == 'commit':
2038 return 'commit'
2039
2040 lgtm_label = data['labels'].get('Code-Review', {})
2041 if lgtm_label:
2042 if 'rejected' in lgtm_label:
2043 return 'not lgtm'
2044 if 'approved' in lgtm_label:
2045 return 'lgtm'
2046
2047 if not data.get('reviewers', {}).get('REVIEWER', []):
2048 return 'unsent'
2049
2050 messages = data.get('messages', [])
2051 if messages:
2052 owner = data['owner'].get('_account_id')
2053 last_message_author = messages[-1].get('author', {}).get('_account_id')
2054 if owner != last_message_author:
2055 # Some reply from non-owner.
2056 return 'reply'
2057
2058 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002059
2060 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002061 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002062 return data['revisions'][data['current_revision']]['_number']
2063
2064 def FetchDescription(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002065 data = self._GetChangeDetail(['COMMIT_FOOTERS', 'CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002066 return data['revisions'][data['current_revision']]['commit_with_footers']
2067
2068 def UpdateDescriptionRemote(self, description):
2069 # TODO(tandrii)
2070 raise NotImplementedError()
2071
2072 def CloseIssue(self):
2073 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2074
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002075 def SubmitIssue(self, wait_for_merge=True):
2076 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2077 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002078
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002079 def _GetChangeDetail(self, options=None, issue=None):
2080 options = options or []
2081 issue = issue or self.GetIssue()
2082 assert issue, 'issue required to query Gerrit'
2083 return gerrit_util.GetChangeDetail(self._GetGerritHost(), options, issue)
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002084
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002085 def CMDLand(self, force, bypass_hooks, verbose):
2086 if git_common.is_dirty_git_tree('land'):
2087 return 1
2088 differs = True
2089 last_upload = RunGit(['config',
2090 'branch.%s.gerritsquashhash' % self.GetBranch()],
2091 error_ok=True).strip()
2092 # Note: git diff outputs nothing if there is no diff.
2093 if not last_upload or RunGit(['diff', last_upload]).strip():
2094 print('WARNING: some changes from local branch haven\'t been uploaded')
2095 else:
2096 detail = self._GetChangeDetail(['CURRENT_REVISION'])
2097 if detail['current_revision'] == last_upload:
2098 differs = False
2099 else:
2100 print('WARNING: local branch contents differ from latest uploaded '
2101 'patchset')
2102 if differs:
2103 if not force:
2104 ask_for_data(
2105 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2106 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2107 elif not bypass_hooks:
2108 hook_results = self.RunHook(
2109 committing=True,
2110 may_prompt=not force,
2111 verbose=verbose,
2112 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2113 if not hook_results.should_continue():
2114 return 1
2115
2116 self.SubmitIssue(wait_for_merge=True)
2117 print('Issue %s has been submitted.' % self.GetIssueURL())
2118 return 0
2119
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002120 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2121 directory):
2122 assert not reject
2123 assert not nocommit
2124 assert not directory
2125 assert parsed_issue_arg.valid
2126
2127 self._changelist.issue = parsed_issue_arg.issue
2128
2129 if parsed_issue_arg.hostname:
2130 self._gerrit_host = parsed_issue_arg.hostname
2131 self._gerrit_server = 'https://%s' % self._gerrit_host
2132
2133 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2134
2135 if not parsed_issue_arg.patchset:
2136 # Use current revision by default.
2137 revision_info = detail['revisions'][detail['current_revision']]
2138 patchset = int(revision_info['_number'])
2139 else:
2140 patchset = parsed_issue_arg.patchset
2141 for revision_info in detail['revisions'].itervalues():
2142 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2143 break
2144 else:
2145 DieWithError('Couldn\'t find patchset %i in issue %i' %
2146 (parsed_issue_arg.patchset, self.GetIssue()))
2147
2148 fetch_info = revision_info['fetch']['http']
2149 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2150 RunGit(['cherry-pick', 'FETCH_HEAD'])
2151 self.SetIssue(self.GetIssue())
2152 self.SetPatchset(patchset)
2153 print('Committed patch for issue %i pathset %i locally' %
2154 (self.GetIssue(), self.GetPatchset()))
2155 return 0
2156
2157 @staticmethod
2158 def ParseIssueURL(parsed_url):
2159 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2160 return None
2161 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2162 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2163 # Short urls like https://domain/<issue_number> can be used, but don't allow
2164 # specifying the patchset (you'd 404), but we allow that here.
2165 if parsed_url.path == '/':
2166 part = parsed_url.fragment
2167 else:
2168 part = parsed_url.path
2169 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2170 if match:
2171 return _ParsedIssueNumberArgument(
2172 issue=int(match.group(2)),
2173 patchset=int(match.group(4)) if match.group(4) else None,
2174 hostname=parsed_url.netloc)
2175 return None
2176
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002177 def CMDUploadChange(self, options, args, change):
2178 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002179 if options.squash and options.no_squash:
2180 DieWithError('Can only use one of --squash or --no-squash')
2181 options.squash = ((settings.GetSquashGerritUploads() or options.squash) and
2182 not options.no_squash)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002183 # We assume the remote called "origin" is the one we want.
2184 # It is probably not worthwhile to support different workflows.
2185 gerrit_remote = 'origin'
2186
2187 remote, remote_branch = self.GetRemoteBranch()
2188 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2189 pending_prefix='')
2190
2191 if options.title:
2192 # TODO(tandrii): it's now supported by Gerrit, implement!
2193 print "\nPatch titles (-t) are not supported in Gerrit. Aborting..."
2194 return 1
2195
2196 if options.squash:
2197 if not self.GetIssue():
2198 # TODO(tandrii): deperecate this after 2016Q2. Backwards compatibility
2199 # with shadow branch, which used to contain change-id for a given
2200 # branch, using which we can fetch actual issue number and set it as the
2201 # property of the branch, which is the new way.
2202 message = RunGitSilent([
2203 'show', '--format=%B', '-s',
2204 'refs/heads/git_cl_uploads/%s' % self.GetBranch()])
2205 if message:
2206 change_ids = git_footers.get_footer_change_id(message.strip())
2207 if change_ids and len(change_ids) == 1:
2208 details = self._GetChangeDetail(issue=change_ids[0])
2209 if details:
2210 print('WARNING: found old upload in branch git_cl_uploads/%s '
2211 'corresponding to issue %s' %
2212 (self.GetBranch(), details['_number']))
2213 self.SetIssue(details['_number'])
2214 if not self.GetIssue():
2215 DieWithError(
2216 '\n' # For readability of the blob below.
2217 'Found old upload in branch git_cl_uploads/%s, '
2218 'but failed to find corresponding Gerrit issue.\n'
2219 'If you know the issue number, set it manually first:\n'
2220 ' git cl issue 123456\n'
2221 'If you intended to upload this CL as new issue, '
2222 'just delete or rename the old upload branch:\n'
2223 ' git rename-branch git_cl_uploads/%s old_upload-%s\n'
2224 'After that, please run git cl upload again.' %
2225 tuple([self.GetBranch()] * 3))
2226 # End of backwards compatability.
2227
2228 if self.GetIssue():
2229 # Try to get the message from a previous upload.
2230 message = self.GetDescription()
2231 if not message:
2232 DieWithError(
2233 'failed to fetch description from current Gerrit issue %d\n'
2234 '%s' % (self.GetIssue(), self.GetIssueURL()))
2235 change_id = self._GetChangeDetail()['change_id']
2236 while True:
2237 footer_change_ids = git_footers.get_footer_change_id(message)
2238 if footer_change_ids == [change_id]:
2239 break
2240 if not footer_change_ids:
2241 message = git_footers.add_footer_change_id(message, change_id)
2242 print('WARNING: appended missing Change-Id to issue description')
2243 continue
2244 # There is already a valid footer but with different or several ids.
2245 # Doing this automatically is non-trivial as we don't want to lose
2246 # existing other footers, yet we want to append just 1 desired
2247 # Change-Id. Thus, just create a new footer, but let user verify the
2248 # new description.
2249 message = '%s\n\nChange-Id: %s' % (message, change_id)
2250 print(
2251 'WARNING: issue %s has Change-Id footer(s):\n'
2252 ' %s\n'
2253 'but issue has Change-Id %s, according to Gerrit.\n'
2254 'Please, check the proposed correction to the description, '
2255 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2256 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2257 change_id))
2258 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2259 if not options.force:
2260 change_desc = ChangeDescription(message)
2261 change_desc.prompt()
2262 message = change_desc.description
2263 if not message:
2264 DieWithError("Description is empty. Aborting...")
2265 # Continue the while loop.
2266 # Sanity check of this code - we should end up with proper message
2267 # footer.
2268 assert [change_id] == git_footers.get_footer_change_id(message)
2269 change_desc = ChangeDescription(message)
2270 else:
2271 change_desc = ChangeDescription(
2272 options.message or CreateDescriptionFromLog(args))
2273 if not options.force:
2274 change_desc.prompt()
2275 if not change_desc.description:
2276 DieWithError("Description is empty. Aborting...")
2277 message = change_desc.description
2278 change_ids = git_footers.get_footer_change_id(message)
2279 if len(change_ids) > 1:
2280 DieWithError('too many Change-Id footers, at most 1 allowed.')
2281 if not change_ids:
2282 # Generate the Change-Id automatically.
2283 message = git_footers.add_footer_change_id(
2284 message, GenerateGerritChangeId(message))
2285 change_desc.set_description(message)
2286 change_ids = git_footers.get_footer_change_id(message)
2287 assert len(change_ids) == 1
2288 change_id = change_ids[0]
2289
2290 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2291 if remote is '.':
2292 # If our upstream branch is local, we base our squashed commit on its
2293 # squashed version.
2294 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2295 # Check the squashed hash of the parent.
2296 parent = RunGit(['config',
2297 'branch.%s.gerritsquashhash' % upstream_branch_name],
2298 error_ok=True).strip()
2299 # Verify that the upstream branch has been uploaded too, otherwise
2300 # Gerrit will create additional CLs when uploading.
2301 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2302 RunGitSilent(['rev-parse', parent + ':'])):
2303 # TODO(tandrii): remove "old depot_tools" part on April 12, 2016.
2304 DieWithError(
2305 'Upload upstream branch %s first.\n'
2306 'Note: maybe you\'ve uploaded it with --no-squash or with an old '
2307 'version of depot_tools. If so, then re-upload it with:\n'
2308 ' git cl upload --squash\n' % upstream_branch_name)
2309 else:
2310 parent = self.GetCommonAncestorWithUpstream()
2311
2312 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2313 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2314 '-m', message]).strip()
2315 else:
2316 change_desc = ChangeDescription(
2317 options.message or CreateDescriptionFromLog(args))
2318 if not change_desc.description:
2319 DieWithError("Description is empty. Aborting...")
2320
2321 if not git_footers.get_footer_change_id(change_desc.description):
2322 DownloadGerritHook(False)
2323 change_desc.set_description(AddChangeIdToCommitMessage(options, args))
2324 ref_to_push = 'HEAD'
2325 parent = '%s/%s' % (gerrit_remote, branch)
2326 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2327
2328 assert change_desc
2329 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2330 ref_to_push)]).splitlines()
2331 if len(commits) > 1:
2332 print('WARNING: This will upload %d commits. Run the following command '
2333 'to see which commits will be uploaded: ' % len(commits))
2334 print('git log %s..%s' % (parent, ref_to_push))
2335 print('You can also use `git squash-branch` to squash these into a '
2336 'single commit.')
2337 ask_for_data('About to upload; enter to confirm.')
2338
2339 if options.reviewers or options.tbr_owners:
2340 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2341 change)
2342
2343 receive_options = []
2344 cc = self.GetCCList().split(',')
2345 if options.cc:
2346 cc.extend(options.cc)
2347 cc = filter(None, cc)
2348 if cc:
2349 receive_options += ['--cc=' + email for email in cc]
2350 if change_desc.get_reviewers():
2351 receive_options.extend(
2352 '--reviewer=' + email for email in change_desc.get_reviewers())
2353
2354 git_command = ['push']
2355 if receive_options:
2356 git_command.append('--receive-pack=git receive-pack %s' %
2357 ' '.join(receive_options))
2358 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
2359 push_stdout = gclient_utils.CheckCallAndFilter(
2360 ['git'] + git_command,
2361 print_stdout=True,
2362 # Flush after every line: useful for seeing progress when running as
2363 # recipe.
2364 filter_fn=lambda _: sys.stdout.flush())
2365
2366 if options.squash:
2367 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2368 change_numbers = [m.group(1)
2369 for m in map(regex.match, push_stdout.splitlines())
2370 if m]
2371 if len(change_numbers) != 1:
2372 DieWithError(
2373 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2374 'Change-Id: %s') % (len(change_numbers), change_id))
2375 self.SetIssue(change_numbers[0])
2376 RunGit(['config', 'branch.%s.gerritsquashhash' % self.GetBranch(),
2377 ref_to_push])
2378 return 0
2379
2380
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002381
2382_CODEREVIEW_IMPLEMENTATIONS = {
2383 'rietveld': _RietveldChangelistImpl,
2384 'gerrit': _GerritChangelistImpl,
2385}
2386
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002387
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002388class ChangeDescription(object):
2389 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002390 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002391 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002392
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002393 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002394 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002395
agable@chromium.org42c20792013-09-12 17:34:49 +00002396 @property # www.logilab.org/ticket/89786
2397 def description(self): # pylint: disable=E0202
2398 return '\n'.join(self._description_lines)
2399
2400 def set_description(self, desc):
2401 if isinstance(desc, basestring):
2402 lines = desc.splitlines()
2403 else:
2404 lines = [line.rstrip() for line in desc]
2405 while lines and not lines[0]:
2406 lines.pop(0)
2407 while lines and not lines[-1]:
2408 lines.pop(-1)
2409 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002410
piman@chromium.org336f9122014-09-04 02:16:55 +00002411 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002412 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002413 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002414 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002415 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002416 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002417
agable@chromium.org42c20792013-09-12 17:34:49 +00002418 # Get the set of R= and TBR= lines and remove them from the desciption.
2419 regexp = re.compile(self.R_LINE)
2420 matches = [regexp.match(line) for line in self._description_lines]
2421 new_desc = [l for i, l in enumerate(self._description_lines)
2422 if not matches[i]]
2423 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002424
agable@chromium.org42c20792013-09-12 17:34:49 +00002425 # Construct new unified R= and TBR= lines.
2426 r_names = []
2427 tbr_names = []
2428 for match in matches:
2429 if not match:
2430 continue
2431 people = cleanup_list([match.group(2).strip()])
2432 if match.group(1) == 'TBR':
2433 tbr_names.extend(people)
2434 else:
2435 r_names.extend(people)
2436 for name in r_names:
2437 if name not in reviewers:
2438 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002439 if add_owners_tbr:
2440 owners_db = owners.Database(change.RepositoryRoot(),
2441 fopen=file, os_path=os.path, glob=glob.glob)
2442 all_reviewers = set(tbr_names + reviewers)
2443 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2444 all_reviewers)
2445 tbr_names.extend(owners_db.reviewers_for(missing_files,
2446 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002447 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2448 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2449
2450 # Put the new lines in the description where the old first R= line was.
2451 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2452 if 0 <= line_loc < len(self._description_lines):
2453 if new_tbr_line:
2454 self._description_lines.insert(line_loc, new_tbr_line)
2455 if new_r_line:
2456 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002457 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002458 if new_r_line:
2459 self.append_footer(new_r_line)
2460 if new_tbr_line:
2461 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002462
2463 def prompt(self):
2464 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002465 self.set_description([
2466 '# Enter a description of the change.',
2467 '# This will be displayed on the codereview site.',
2468 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002469 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002470 '--------------------',
2471 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002472
agable@chromium.org42c20792013-09-12 17:34:49 +00002473 regexp = re.compile(self.BUG_LINE)
2474 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00002475 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00002476 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002477 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002478 if not content:
2479 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002480 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002481
2482 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002483 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2484 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002485 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002486 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002487
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002488 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00002489 if self._description_lines:
2490 # Add an empty line if either the last line or the new line isn't a tag.
2491 last_line = self._description_lines[-1]
2492 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
2493 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2494 self._description_lines.append('')
2495 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002496
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002497 def get_reviewers(self):
2498 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002499 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
2500 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002501 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002502
2503
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002504def get_approving_reviewers(props):
2505 """Retrieves the reviewers that approved a CL from the issue properties with
2506 messages.
2507
2508 Note that the list may contain reviewers that are not committer, thus are not
2509 considered by the CQ.
2510 """
2511 return sorted(
2512 set(
2513 message['sender']
2514 for message in props['messages']
2515 if message['approval'] and message['sender'] in props['reviewers']
2516 )
2517 )
2518
2519
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002520def FindCodereviewSettingsFile(filename='codereview.settings'):
2521 """Finds the given file starting in the cwd and going up.
2522
2523 Only looks up to the top of the repository unless an
2524 'inherit-review-settings-ok' file exists in the root of the repository.
2525 """
2526 inherit_ok_file = 'inherit-review-settings-ok'
2527 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002528 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002529 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2530 root = '/'
2531 while True:
2532 if filename in os.listdir(cwd):
2533 if os.path.isfile(os.path.join(cwd, filename)):
2534 return open(os.path.join(cwd, filename))
2535 if cwd == root:
2536 break
2537 cwd = os.path.dirname(cwd)
2538
2539
2540def LoadCodereviewSettingsFromFile(fileobj):
2541 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002542 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002543
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002544 def SetProperty(name, setting, unset_error_ok=False):
2545 fullname = 'rietveld.' + name
2546 if setting in keyvals:
2547 RunGit(['config', fullname, keyvals[setting]])
2548 else:
2549 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2550
2551 SetProperty('server', 'CODE_REVIEW_SERVER')
2552 # Only server setting is required. Other settings can be absent.
2553 # In that case, we ignore errors raised during option deletion attempt.
2554 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002555 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002556 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2557 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002558 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002559 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002560 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2561 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002562 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002563 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002564 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002565 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2566 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002567
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002568 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002569 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002570
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002571 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
2572 RunGit(['config', 'gerrit.squash-uploads',
2573 keyvals['GERRIT_SQUASH_UPLOADS']])
2574
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002575 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2576 #should be of the form
2577 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2578 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2579 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2580 keyvals['ORIGIN_URL_CONFIG']])
2581
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002582
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002583def urlretrieve(source, destination):
2584 """urllib is broken for SSL connections via a proxy therefore we
2585 can't use urllib.urlretrieve()."""
2586 with open(destination, 'w') as f:
2587 f.write(urllib2.urlopen(source).read())
2588
2589
ukai@chromium.org712d6102013-11-27 00:52:58 +00002590def hasSheBang(fname):
2591 """Checks fname is a #! script."""
2592 with open(fname) as f:
2593 return f.read(2).startswith('#!')
2594
2595
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00002596# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
2597def DownloadHooks(*args, **kwargs):
2598 pass
2599
2600
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002601def DownloadGerritHook(force):
2602 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002603
2604 Args:
2605 force: True to update hooks. False to install hooks if not present.
2606 """
2607 if not settings.GetIsGerrit():
2608 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00002609 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002610 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2611 if not os.access(dst, os.X_OK):
2612 if os.path.exists(dst):
2613 if not force:
2614 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002615 try:
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002616 print(
2617 'WARNING: installing Gerrit commit-msg hook.\n'
2618 ' This behavior of git cl will soon be disabled.\n'
2619 ' See bug http://crbug.com/579176.')
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002620 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002621 if not hasSheBang(dst):
2622 DieWithError('Not a script: %s\n'
2623 'You need to download from\n%s\n'
2624 'into .git/hooks/commit-msg and '
2625 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002626 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2627 except Exception:
2628 if os.path.exists(dst):
2629 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002630 DieWithError('\nFailed to download hooks.\n'
2631 'You need to download from\n%s\n'
2632 'into .git/hooks/commit-msg and '
2633 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002634
2635
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002636
2637def GetRietveldCodereviewSettingsInteractively():
2638 """Prompt the user for settings."""
2639 server = settings.GetDefaultServerUrl(error_ok=True)
2640 prompt = 'Rietveld server (host[:port])'
2641 prompt += ' [%s]' % (server or DEFAULT_SERVER)
2642 newserver = ask_for_data(prompt + ':')
2643 if not server and not newserver:
2644 newserver = DEFAULT_SERVER
2645 if newserver:
2646 newserver = gclient_utils.UpgradeToHttps(newserver)
2647 if newserver != server:
2648 RunGit(['config', 'rietveld.server', newserver])
2649
2650 def SetProperty(initial, caption, name, is_url):
2651 prompt = caption
2652 if initial:
2653 prompt += ' ("x" to clear) [%s]' % initial
2654 new_val = ask_for_data(prompt + ':')
2655 if new_val == 'x':
2656 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
2657 elif new_val:
2658 if is_url:
2659 new_val = gclient_utils.UpgradeToHttps(new_val)
2660 if new_val != initial:
2661 RunGit(['config', 'rietveld.' + name, new_val])
2662
2663 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
2664 SetProperty(settings.GetDefaultPrivateFlag(),
2665 'Private flag (rietveld only)', 'private', False)
2666 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
2667 'tree-status-url', False)
2668 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
2669 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
2670 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
2671 'run-post-upload-hook', False)
2672
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002673@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002674def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002675 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002676
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002677 print('WARNING: git cl config works for Rietveld only.\n'
2678 'For Gerrit, see http://crbug.com/579160.')
2679 # TODO(tandrii): add Gerrit support as part of http://crbug.com/579160.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00002680 parser.add_option('--activate-update', action='store_true',
2681 help='activate auto-updating [rietveld] section in '
2682 '.git/config')
2683 parser.add_option('--deactivate-update', action='store_true',
2684 help='deactivate auto-updating [rietveld] section in '
2685 '.git/config')
2686 options, args = parser.parse_args(args)
2687
2688 if options.deactivate_update:
2689 RunGit(['config', 'rietveld.autoupdate', 'false'])
2690 return
2691
2692 if options.activate_update:
2693 RunGit(['config', '--unset', 'rietveld.autoupdate'])
2694 return
2695
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002696 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002697 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002698 return 0
2699
2700 url = args[0]
2701 if not url.endswith('codereview.settings'):
2702 url = os.path.join(url, 'codereview.settings')
2703
2704 # Load code review settings and download hooks (if available).
2705 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
2706 return 0
2707
2708
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002709def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002710 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002711 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
2712 branch = ShortBranchName(branchref)
2713 _, args = parser.parse_args(args)
2714 if not args:
2715 print("Current base-url:")
2716 return RunGit(['config', 'branch.%s.base-url' % branch],
2717 error_ok=False).strip()
2718 else:
2719 print("Setting base-url to %s" % args[0])
2720 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
2721 error_ok=False).strip()
2722
2723
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002724def color_for_status(status):
2725 """Maps a Changelist status to color, for CMDstatus and other tools."""
2726 return {
2727 'unsent': Fore.RED,
2728 'waiting': Fore.BLUE,
2729 'reply': Fore.YELLOW,
2730 'lgtm': Fore.GREEN,
2731 'commit': Fore.MAGENTA,
2732 'closed': Fore.CYAN,
2733 'error': Fore.WHITE,
2734 }.get(status, Fore.WHITE)
2735
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002736def fetch_cl_status(branch, auth_config=None):
2737 """Fetches information for an issue and returns (branch, issue, status)."""
2738 cl = Changelist(branchref=branch, auth_config=auth_config)
2739 url = cl.GetIssueURL()
2740 status = cl.GetStatus()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002741
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002742 if url and (not status or status == 'error'):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002743 # The issue probably doesn't exist anymore.
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002744 url += ' (broken)'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002745
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002746 return (branch, url, status)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002747
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002748def get_cl_statuses(
2749 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002750 """Returns a blocking iterable of (branch, issue, color) for given branches.
2751
2752 If fine_grained is true, this will fetch CL statuses from the server.
2753 Otherwise, simply indicate if there's a matching url for the given branches.
2754
2755 If max_processes is specified, it is used as the maximum number of processes
2756 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
2757 spawned.
2758 """
2759 # Silence upload.py otherwise it becomes unwieldly.
2760 upload.verbosity = 0
2761
2762 if fine_grained:
2763 # Process one branch synchronously to work through authentication, then
2764 # spawn processes to process all the other branches in parallel.
2765 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002766 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
2767 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002768
2769 branches_to_fetch = branches[1:]
2770 pool = ThreadPool(
2771 min(max_processes, len(branches_to_fetch))
2772 if max_processes is not None
2773 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002774 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002775 yield x
2776 else:
2777 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
2778 for b in branches:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002779 cl = Changelist(branchref=b, auth_config=auth_config)
2780 url = cl.GetIssueURL()
2781 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002782
rmistry@google.com2dd99862015-06-22 12:22:18 +00002783
2784def upload_branch_deps(cl, args):
2785 """Uploads CLs of local branches that are dependents of the current branch.
2786
2787 If the local branch dependency tree looks like:
2788 test1 -> test2.1 -> test3.1
2789 -> test3.2
2790 -> test2.2 -> test3.3
2791
2792 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
2793 run on the dependent branches in this order:
2794 test2.1, test3.1, test3.2, test2.2, test3.3
2795
2796 Note: This function does not rebase your local dependent branches. Use it when
2797 you make a change to the parent branch that will not conflict with its
2798 dependent branches, and you would like their dependencies updated in
2799 Rietveld.
2800 """
2801 if git_common.is_dirty_git_tree('upload-branch-deps'):
2802 return 1
2803
2804 root_branch = cl.GetBranch()
2805 if root_branch is None:
2806 DieWithError('Can\'t find dependent branches from detached HEAD state. '
2807 'Get on a branch!')
2808 if not cl.GetIssue() or not cl.GetPatchset():
2809 DieWithError('Current branch does not have an uploaded CL. We cannot set '
2810 'patchset dependencies without an uploaded CL.')
2811
2812 branches = RunGit(['for-each-ref',
2813 '--format=%(refname:short) %(upstream:short)',
2814 'refs/heads'])
2815 if not branches:
2816 print('No local branches found.')
2817 return 0
2818
2819 # Create a dictionary of all local branches to the branches that are dependent
2820 # on it.
2821 tracked_to_dependents = collections.defaultdict(list)
2822 for b in branches.splitlines():
2823 tokens = b.split()
2824 if len(tokens) == 2:
2825 branch_name, tracked = tokens
2826 tracked_to_dependents[tracked].append(branch_name)
2827
2828 print
2829 print 'The dependent local branches of %s are:' % root_branch
2830 dependents = []
2831 def traverse_dependents_preorder(branch, padding=''):
2832 dependents_to_process = tracked_to_dependents.get(branch, [])
2833 padding += ' '
2834 for dependent in dependents_to_process:
2835 print '%s%s' % (padding, dependent)
2836 dependents.append(dependent)
2837 traverse_dependents_preorder(dependent, padding)
2838 traverse_dependents_preorder(root_branch)
2839 print
2840
2841 if not dependents:
2842 print 'There are no dependent local branches for %s' % root_branch
2843 return 0
2844
2845 print ('This command will checkout all dependent branches and run '
2846 '"git cl upload".')
2847 ask_for_data('[Press enter to continue or ctrl-C to quit]')
2848
andybons@chromium.org962f9462016-02-03 20:00:42 +00002849 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00002850 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00002851 args.extend(['-t', 'Updated patchset dependency'])
2852
rmistry@google.com2dd99862015-06-22 12:22:18 +00002853 # Record all dependents that failed to upload.
2854 failures = {}
2855 # Go through all dependents, checkout the branch and upload.
2856 try:
2857 for dependent_branch in dependents:
2858 print
2859 print '--------------------------------------'
2860 print 'Running "git cl upload" from %s:' % dependent_branch
2861 RunGit(['checkout', '-q', dependent_branch])
2862 print
2863 try:
2864 if CMDupload(OptionParser(), args) != 0:
2865 print 'Upload failed for %s!' % dependent_branch
2866 failures[dependent_branch] = 1
2867 except: # pylint: disable=W0702
2868 failures[dependent_branch] = 1
2869 print
2870 finally:
2871 # Swap back to the original root branch.
2872 RunGit(['checkout', '-q', root_branch])
2873
2874 print
2875 print 'Upload complete for dependent branches!'
2876 for dependent_branch in dependents:
2877 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
2878 print ' %s : %s' % (dependent_branch, upload_status)
2879 print
2880
2881 return 0
2882
2883
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002884def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002885 """Show status of changelists.
2886
2887 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00002888 - Red not sent for review or broken
2889 - Blue waiting for review
2890 - Yellow waiting for you to reply to review
2891 - Green LGTM'ed
2892 - Magenta in the commit queue
2893 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002894
2895 Also see 'git cl comments'.
2896 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002897 parser.add_option('--field',
2898 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002899 parser.add_option('-f', '--fast', action='store_true',
2900 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002901 parser.add_option(
2902 '-j', '--maxjobs', action='store', type=int,
2903 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002904
2905 auth.add_auth_options(parser)
2906 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002907 if args:
2908 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002909 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002910
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002911 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002912 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002913 if options.field.startswith('desc'):
2914 print cl.GetDescription()
2915 elif options.field == 'id':
2916 issueid = cl.GetIssue()
2917 if issueid:
2918 print issueid
2919 elif options.field == 'patch':
2920 patchset = cl.GetPatchset()
2921 if patchset:
2922 print patchset
2923 elif options.field == 'url':
2924 url = cl.GetIssueURL()
2925 if url:
2926 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002927 return 0
2928
2929 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
2930 if not branches:
2931 print('No local branch found.')
2932 return 0
2933
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002934 changes = (
2935 Changelist(branchref=b, auth_config=auth_config)
2936 for b in branches.splitlines())
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002937 # TODO(tandrii): refactor to use CLs list instead of branches list.
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00002938 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002939 alignment = max(5, max(len(b) for b in branches))
2940 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002941 output = get_cl_statuses(branches,
2942 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002943 max_processes=options.maxjobs,
2944 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002945
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002946 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002947 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002948 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002949 while branch not in branch_statuses:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002950 b, i, status = output.next()
2951 branch_statuses[b] = (i, status)
2952 issue_url, status = branch_statuses.pop(branch)
2953 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00002954 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00002955 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00002956 color = ''
2957 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002958 status_str = '(%s)' % status if status else ''
2959 print ' %*s : %s%s %s%s' % (
2960 alignment, ShortBranchName(branch), color, issue_url, status_str,
2961 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002962
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002963 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002964 print
2965 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002966 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00002967 if not cl.GetIssue():
2968 print 'No issue assigned.'
2969 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002970 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00002971 if not options.fast:
2972 print 'Issue description:'
2973 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002974 return 0
2975
2976
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002977def colorize_CMDstatus_doc():
2978 """To be called once in main() to add colors to git cl status help."""
2979 colors = [i for i in dir(Fore) if i[0].isupper()]
2980
2981 def colorize_line(line):
2982 for color in colors:
2983 if color in line.upper():
2984 # Extract whitespaces first and the leading '-'.
2985 indent = len(line) - len(line.lstrip(' ')) + 1
2986 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
2987 return line
2988
2989 lines = CMDstatus.__doc__.splitlines()
2990 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
2991
2992
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002993@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002994def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002995 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002996
2997 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002998 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00002999 parser.add_option('-r', '--reverse', action='store_true',
3000 help='Lookup the branch(es) for the specified issues. If '
3001 'no issues are specified, all branches with mapped '
3002 'issues will be listed.')
3003 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003004
dnj@chromium.org406c4402015-03-03 17:22:28 +00003005 if options.reverse:
3006 branches = RunGit(['for-each-ref', 'refs/heads',
3007 '--format=%(refname:short)']).splitlines()
3008
3009 # Reverse issue lookup.
3010 issue_branch_map = {}
3011 for branch in branches:
3012 cl = Changelist(branchref=branch)
3013 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3014 if not args:
3015 args = sorted(issue_branch_map.iterkeys())
3016 for issue in args:
3017 if not issue:
3018 continue
3019 print 'Branch for issue number %s: %s' % (
3020 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
3021 else:
3022 cl = Changelist()
3023 if len(args) > 0:
3024 try:
3025 issue = int(args[0])
3026 except ValueError:
3027 DieWithError('Pass a number to set the issue or none to list it.\n'
3028 'Maybe you want to run git cl status?')
3029 cl.SetIssue(issue)
3030 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003031 return 0
3032
3033
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003034def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003035 """Shows or posts review comments for any changelist."""
3036 parser.add_option('-a', '--add-comment', dest='comment',
3037 help='comment to add to an issue')
3038 parser.add_option('-i', dest='issue',
3039 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003040 parser.add_option('-j', '--json-file',
3041 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003042 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003043 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003044 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003045
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003046 issue = None
3047 if options.issue:
3048 try:
3049 issue = int(options.issue)
3050 except ValueError:
3051 DieWithError('A review issue id is expected to be a number')
3052
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003053 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003054
3055 if options.comment:
3056 cl.AddComment(options.comment)
3057 return 0
3058
3059 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003060 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003061 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003062 summary.append({
3063 'date': message['date'],
3064 'lgtm': False,
3065 'message': message['text'],
3066 'not_lgtm': False,
3067 'sender': message['sender'],
3068 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003069 if message['disapproval']:
3070 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003071 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003072 elif message['approval']:
3073 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003074 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003075 elif message['sender'] == data['owner_email']:
3076 color = Fore.MAGENTA
3077 else:
3078 color = Fore.BLUE
3079 print '\n%s%s %s%s' % (
3080 color, message['date'].split('.', 1)[0], message['sender'],
3081 Fore.RESET)
3082 if message['text'].strip():
3083 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00003084 if options.json_file:
3085 with open(options.json_file, 'wb') as f:
3086 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003087 return 0
3088
3089
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003090def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003091 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003092 parser.add_option('-d', '--display', action='store_true',
3093 help='Display the description instead of opening an editor')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003094 auth.add_auth_options(parser)
3095 options, _ = parser.parse_args(args)
3096 auth_config = auth.extract_auth_config_from_options(options)
3097 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003098 if not cl.GetIssue():
3099 DieWithError('This branch has no associated changelist.')
3100 description = ChangeDescription(cl.GetDescription())
smut@google.com34fb6b12015-07-13 20:03:26 +00003101 if options.display:
3102 print description.description
3103 return 0
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003104 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003105 if cl.GetDescription() != description.description:
3106 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003107 return 0
3108
3109
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003110def CreateDescriptionFromLog(args):
3111 """Pulls out the commit log to use as a base for the CL description."""
3112 log_args = []
3113 if len(args) == 1 and not args[0].endswith('.'):
3114 log_args = [args[0] + '..']
3115 elif len(args) == 1 and args[0].endswith('...'):
3116 log_args = [args[0][:-1]]
3117 elif len(args) == 2:
3118 log_args = [args[0] + '..' + args[1]]
3119 else:
3120 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003121 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003122
3123
thestig@chromium.org44202a22014-03-11 19:22:18 +00003124def CMDlint(parser, args):
3125 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003126 parser.add_option('--filter', action='append', metavar='-x,+y',
3127 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003128 auth.add_auth_options(parser)
3129 options, args = parser.parse_args(args)
3130 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003131
3132 # Access to a protected member _XX of a client class
3133 # pylint: disable=W0212
3134 try:
3135 import cpplint
3136 import cpplint_chromium
3137 except ImportError:
3138 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
3139 return 1
3140
3141 # Change the current working directory before calling lint so that it
3142 # shows the correct base.
3143 previous_cwd = os.getcwd()
3144 os.chdir(settings.GetRoot())
3145 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003146 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003147 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3148 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003149 if not files:
3150 print "Cannot lint an empty CL"
3151 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003152
3153 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003154 command = args + files
3155 if options.filter:
3156 command = ['--filter=' + ','.join(options.filter)] + command
3157 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003158
3159 white_regex = re.compile(settings.GetLintRegex())
3160 black_regex = re.compile(settings.GetLintIgnoreRegex())
3161 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3162 for filename in filenames:
3163 if white_regex.match(filename):
3164 if black_regex.match(filename):
3165 print "Ignoring file %s" % filename
3166 else:
3167 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3168 extra_check_functions)
3169 else:
3170 print "Skipping file %s" % filename
3171 finally:
3172 os.chdir(previous_cwd)
3173 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
3174 if cpplint._cpplint_state.error_count != 0:
3175 return 1
3176 return 0
3177
3178
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003179def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003180 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003181 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003182 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003183 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003184 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003185 auth.add_auth_options(parser)
3186 options, args = parser.parse_args(args)
3187 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003188
sbc@chromium.org71437c02015-04-09 19:29:40 +00003189 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00003190 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003191 return 1
3192
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003193 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003194 if args:
3195 base_branch = args[0]
3196 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003197 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003198 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003199
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003200 cl.RunHook(
3201 committing=not options.upload,
3202 may_prompt=False,
3203 verbose=options.verbose,
3204 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003205 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003206
3207
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00003208def AddChangeIdToCommitMessage(options, args):
3209 """Re-commits using the current message, assumes the commit hook is in
3210 place.
3211 """
3212 log_desc = options.message or CreateDescriptionFromLog(args)
3213 git_command = ['commit', '--amend', '-m', log_desc]
3214 RunGit(git_command)
3215 new_log_desc = CreateDescriptionFromLog(args)
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003216 if git_footers.get_footer_change_id(new_log_desc):
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00003217 print 'git-cl: Added Change-Id to commit message.'
tandrii@chromium.orga342c922016-03-16 07:08:25 +00003218 return new_log_desc
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00003219 else:
3220 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
3221
3222
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003223def GenerateGerritChangeId(message):
3224 """Returns Ixxxxxx...xxx change id.
3225
3226 Works the same way as
3227 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3228 but can be called on demand on all platforms.
3229
3230 The basic idea is to generate git hash of a state of the tree, original commit
3231 message, author/committer info and timestamps.
3232 """
3233 lines = []
3234 tree_hash = RunGitSilent(['write-tree'])
3235 lines.append('tree %s' % tree_hash.strip())
3236 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3237 if code == 0:
3238 lines.append('parent %s' % parent.strip())
3239 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3240 lines.append('author %s' % author.strip())
3241 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3242 lines.append('committer %s' % committer.strip())
3243 lines.append('')
3244 # Note: Gerrit's commit-hook actually cleans message of some lines and
3245 # whitespace. This code is not doing this, but it clearly won't decrease
3246 # entropy.
3247 lines.append(message)
3248 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3249 stdin='\n'.join(lines))
3250 return 'I%s' % change_hash.strip()
3251
3252
wittman@chromium.org455dc922015-01-26 20:15:50 +00003253def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3254 """Computes the remote branch ref to use for the CL.
3255
3256 Args:
3257 remote (str): The git remote for the CL.
3258 remote_branch (str): The git remote branch for the CL.
3259 target_branch (str): The target branch specified by the user.
3260 pending_prefix (str): The pending prefix from the settings.
3261 """
3262 if not (remote and remote_branch):
3263 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003264
wittman@chromium.org455dc922015-01-26 20:15:50 +00003265 if target_branch:
3266 # Cannonicalize branch references to the equivalent local full symbolic
3267 # refs, which are then translated into the remote full symbolic refs
3268 # below.
3269 if '/' not in target_branch:
3270 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3271 else:
3272 prefix_replacements = (
3273 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3274 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3275 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3276 )
3277 match = None
3278 for regex, replacement in prefix_replacements:
3279 match = re.search(regex, target_branch)
3280 if match:
3281 remote_branch = target_branch.replace(match.group(0), replacement)
3282 break
3283 if not match:
3284 # This is a branch path but not one we recognize; use as-is.
3285 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003286 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3287 # Handle the refs that need to land in different refs.
3288 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003289
wittman@chromium.org455dc922015-01-26 20:15:50 +00003290 # Create the true path to the remote branch.
3291 # Does the following translation:
3292 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3293 # * refs/remotes/origin/master -> refs/heads/master
3294 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3295 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3296 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3297 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3298 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3299 'refs/heads/')
3300 elif remote_branch.startswith('refs/remotes/branch-heads'):
3301 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3302 # If a pending prefix exists then replace refs/ with it.
3303 if pending_prefix:
3304 remote_branch = remote_branch.replace('refs/', pending_prefix)
3305 return remote_branch
3306
3307
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003308def cleanup_list(l):
3309 """Fixes a list so that comma separated items are put as individual items.
3310
3311 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3312 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3313 """
3314 items = sum((i.split(',') for i in l), [])
3315 stripped_items = (i.strip() for i in items)
3316 return sorted(filter(None, stripped_items))
3317
3318
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003319@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003320def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003321 """Uploads the current changelist to codereview.
3322
3323 Can skip dependency patchset uploads for a branch by running:
3324 git config branch.branch_name.skip-deps-uploads True
3325 To unset run:
3326 git config --unset branch.branch_name.skip-deps-uploads
3327 Can also set the above globally by using the --global flag.
3328 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003329 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3330 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003331 parser.add_option('--bypass-watchlists', action='store_true',
3332 dest='bypass_watchlists',
3333 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003334 parser.add_option('-f', action='store_true', dest='force',
3335 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003336 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003337 parser.add_option('-t', dest='title',
3338 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003339 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003340 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003341 help='reviewer email addresses')
3342 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003343 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003344 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003345 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003346 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003347 parser.add_option('--emulate_svn_auto_props',
3348 '--emulate-svn-auto-props',
3349 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003350 dest="emulate_svn_auto_props",
3351 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003352 parser.add_option('-c', '--use-commit-queue', action='store_true',
3353 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003354 parser.add_option('--private', action='store_true',
3355 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003356 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003357 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003358 metavar='TARGET',
3359 help='Apply CL to remote ref TARGET. ' +
3360 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003361 parser.add_option('--squash', action='store_true',
3362 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003363 parser.add_option('--no-squash', action='store_true',
3364 help='Don\'t squash multiple commits into one ' +
3365 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003366 parser.add_option('--email', default=None,
3367 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003368 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3369 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003370 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3371 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003372 help='Send the patchset to do a CQ dry run right after '
3373 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003374 parser.add_option('--dependencies', action='store_true',
3375 help='Uploads CLs of all the local branches that depend on '
3376 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003377
rmistry@google.com2dd99862015-06-22 12:22:18 +00003378 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003379 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003380 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003381 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003382 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003383
sbc@chromium.org71437c02015-04-09 19:29:40 +00003384 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003385 return 1
3386
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003387 options.reviewers = cleanup_list(options.reviewers)
3388 options.cc = cleanup_list(options.cc)
3389
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003390 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3391 settings.GetIsGerrit()
3392
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003393 cl = Changelist(auth_config=auth_config)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00003394 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003395
3396
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003397def IsSubmoduleMergeCommit(ref):
3398 # When submodules are added to the repo, we expect there to be a single
3399 # non-git-svn merge commit at remote HEAD with a signature comment.
3400 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003401 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003402 return RunGit(cmd) != ''
3403
3404
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003405def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003406 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003407
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003408 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3409 upstream and closes the issue automatically and atomically.
3410
3411 Otherwise (in case of Rietveld):
3412 Squashes branch into a single commit.
3413 Updates changelog with metadata (e.g. pointer to review).
3414 Pushes/dcommits the code upstream.
3415 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003416 """
3417 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3418 help='bypass upload presubmit hook')
3419 parser.add_option('-m', dest='message',
3420 help="override review description")
3421 parser.add_option('-f', action='store_true', dest='force',
3422 help="force yes to questions (don't prompt)")
3423 parser.add_option('-c', dest='contributor',
3424 help="external contributor for patch (appended to " +
3425 "description and used as author for git). Should be " +
3426 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003427 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003428 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003429 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003430 auth_config = auth.extract_auth_config_from_options(options)
3431
3432 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003433
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003434 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3435 if cl.IsGerrit():
3436 if options.message:
3437 # This could be implemented, but it requires sending a new patch to
3438 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3439 # Besides, Gerrit has the ability to change the commit message on submit
3440 # automatically, thus there is no need to support this option (so far?).
3441 parser.error('-m MESSAGE option is not supported for Gerrit.')
3442 if options.contributor:
3443 parser.error(
3444 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3445 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3446 'the contributor\'s "name <email>". If you can\'t upload such a '
3447 'commit for review, contact your repository admin and request'
3448 '"Forge-Author" permission.')
3449 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3450 options.verbose)
3451
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003452 current = cl.GetBranch()
3453 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3454 if not settings.GetIsGitSvn() and remote == '.':
3455 print
3456 print 'Attempting to push branch %r into another local branch!' % current
3457 print
3458 print 'Either reparent this branch on top of origin/master:'
3459 print ' git reparent-branch --root'
3460 print
3461 print 'OR run `git rebase-update` if you think the parent branch is already'
3462 print 'committed.'
3463 print
3464 print ' Current parent: %r' % upstream_branch
3465 return 1
3466
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003467 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003468 # Default to merging against our best guess of the upstream branch.
3469 args = [cl.GetUpstreamBranch()]
3470
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003471 if options.contributor:
3472 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
3473 print "Please provide contibutor as 'First Last <email@example.com>'"
3474 return 1
3475
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003476 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003477 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003478
sbc@chromium.org71437c02015-04-09 19:29:40 +00003479 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003480 return 1
3481
3482 # This rev-list syntax means "show all commits not in my branch that
3483 # are in base_branch".
3484 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3485 base_branch]).splitlines()
3486 if upstream_commits:
3487 print ('Base branch "%s" has %d commits '
3488 'not in this branch.' % (base_branch, len(upstream_commits)))
3489 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
3490 return 1
3491
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003492 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003493 svn_head = None
3494 if cmd == 'dcommit' or base_has_submodules:
3495 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
3496 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003497
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003498 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003499 # If the base_head is a submodule merge commit, the first parent of the
3500 # base_head should be a git-svn commit, which is what we're interested in.
3501 base_svn_head = base_branch
3502 if base_has_submodules:
3503 base_svn_head += '^1'
3504
3505 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003506 if extra_commits:
3507 print ('This branch has %d additional commits not upstreamed yet.'
3508 % len(extra_commits.splitlines()))
3509 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
3510 'before attempting to %s.' % (base_branch, cmd))
3511 return 1
3512
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003513 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003514 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003515 author = None
3516 if options.contributor:
3517 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003518 hook_results = cl.RunHook(
3519 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003520 may_prompt=not options.force,
3521 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003522 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003523 if not hook_results.should_continue():
3524 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003525
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003526 # Check the tree status if the tree status URL is set.
3527 status = GetTreeStatus()
3528 if 'closed' == status:
3529 print('The tree is closed. Please wait for it to reopen. Use '
3530 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3531 return 1
3532 elif 'unknown' == status:
3533 print('Unable to determine tree status. Please verify manually and '
3534 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3535 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003536
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003537 change_desc = ChangeDescription(options.message)
3538 if not change_desc.description and cl.GetIssue():
3539 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003540
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003541 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00003542 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003543 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00003544 else:
3545 print 'No description set.'
3546 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
3547 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003548
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003549 # Keep a separate copy for the commit message, because the commit message
3550 # contains the link to the Rietveld issue, while the Rietveld message contains
3551 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003552 # Keep a separate copy for the commit message.
3553 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00003554 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003555
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003556 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00003557 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00003558 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00003559 # after it. Add a period on a new line to circumvent this. Also add a space
3560 # before the period to make sure that Gitiles continues to correctly resolve
3561 # the URL.
3562 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003563 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003564 commit_desc.append_footer('Patch from %s.' % options.contributor)
3565
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00003566 print('Description:')
3567 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003568
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003569 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003570 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00003571 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003572
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003573 # We want to squash all this branch's commits into one commit with the proper
3574 # description. We do this by doing a "reset --soft" to the base branch (which
3575 # keeps the working copy the same), then dcommitting that. If origin/master
3576 # has a submodule merge commit, we'll also need to cherry-pick the squashed
3577 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003578 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003579 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
3580 # Delete the branches if they exist.
3581 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
3582 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
3583 result = RunGitWithCode(showref_cmd)
3584 if result[0] == 0:
3585 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003586
3587 # We might be in a directory that's present in this branch but not in the
3588 # trunk. Move up to the top of the tree so that git commands that expect a
3589 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003590 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003591 if rel_base_path:
3592 os.chdir(rel_base_path)
3593
3594 # Stuff our change into the merge branch.
3595 # We wrap in a try...finally block so if anything goes wrong,
3596 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003597 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003598 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003599 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003600 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003601 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00003602 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003603 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003604 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003605 RunGit(
3606 [
3607 'commit', '--author', options.contributor,
3608 '-m', commit_desc.description,
3609 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003610 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003611 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003612 if base_has_submodules:
3613 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
3614 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
3615 RunGit(['checkout', CHERRY_PICK_BRANCH])
3616 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003617 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003618 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003619 mirror = settings.GetGitMirror(remote)
3620 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003621 pending_prefix = settings.GetPendingRefPrefix()
3622 if not pending_prefix or branch.startswith(pending_prefix):
3623 # If not using refs/pending/heads/* at all, or target ref is already set
3624 # to pending, then push to the target ref directly.
3625 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003626 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003627 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003628 else:
3629 # Cherry-pick the change on top of pending ref and then push it.
3630 assert branch.startswith('refs/'), branch
3631 assert pending_prefix[-1] == '/', pending_prefix
3632 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003633 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003634 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003635 if retcode == 0:
3636 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003637 else:
3638 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003639 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003640 'svn', 'dcommit',
3641 '-C%s' % options.similarity,
3642 '--no-rebase', '--rmdir',
3643 ]
3644 if settings.GetForceHttpsCommitUrl():
3645 # Allow forcing https commit URLs for some projects that don't allow
3646 # committing to http URLs (like Google Code).
3647 remote_url = cl.GetGitSvnRemoteUrl()
3648 if urlparse.urlparse(remote_url).scheme == 'http':
3649 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003650 cmd_args.append('--commit-url=%s' % remote_url)
3651 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003652 if 'Committed r' in output:
3653 revision = re.match(
3654 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
3655 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003656 finally:
3657 # And then swap back to the original branch and clean up.
3658 RunGit(['checkout', '-q', cl.GetBranch()])
3659 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003660 if base_has_submodules:
3661 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003662
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003663 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003664 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003665 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003666
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003667 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003668 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003669 try:
3670 revision = WaitForRealCommit(remote, revision, base_branch, branch)
3671 # We set pushed_to_pending to False, since it made it all the way to the
3672 # real ref.
3673 pushed_to_pending = False
3674 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003675 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003676
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003677 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003678 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003679 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003680 if not to_pending:
3681 if viewvc_url and revision:
3682 change_desc.append_footer(
3683 'Committed: %s%s' % (viewvc_url, revision))
3684 elif revision:
3685 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003686 print ('Closing issue '
3687 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003688 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003689 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003690 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00003691 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00003692 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00003693 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003694 if options.bypass_hooks:
3695 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
3696 else:
3697 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00003698 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003699 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003700
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003701 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003702 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
3703 print 'The commit is in the pending queue (%s).' % pending_ref
3704 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00003705 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003706 'footer.' % branch)
3707
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003708 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
3709 if os.path.isfile(hook):
3710 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003711
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003712 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003713
3714
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003715def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
3716 print
3717 print 'Waiting for commit to be landed on %s...' % real_ref
3718 print '(If you are impatient, you may Ctrl-C once without harm)'
3719 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
3720 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003721 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003722
3723 loop = 0
3724 while True:
3725 sys.stdout.write('fetching (%d)... \r' % loop)
3726 sys.stdout.flush()
3727 loop += 1
3728
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003729 if mirror:
3730 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003731 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
3732 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
3733 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
3734 for commit in commits.splitlines():
3735 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
3736 print 'Found commit on %s' % real_ref
3737 return commit
3738
3739 current_rev = to_rev
3740
3741
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003742def PushToGitPending(remote, pending_ref, upstream_ref):
3743 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
3744
3745 Returns:
3746 (retcode of last operation, output log of last operation).
3747 """
3748 assert pending_ref.startswith('refs/'), pending_ref
3749 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
3750 cherry = RunGit(['rev-parse', 'HEAD']).strip()
3751 code = 0
3752 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003753 max_attempts = 3
3754 attempts_left = max_attempts
3755 while attempts_left:
3756 if attempts_left != max_attempts:
3757 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
3758 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003759
3760 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003761 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003762 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003763 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003764 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003765 print 'Fetch failed with exit code %d.' % code
3766 if out.strip():
3767 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003768 continue
3769
3770 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003771 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003772 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003773 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003774 if code:
3775 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003776 'Your patch doesn\'t apply cleanly to ref \'%s\', '
3777 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003778 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
3779 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003780 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003781 return code, out
3782
3783 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003784 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003785 code, out = RunGitWithCode(
3786 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
3787 if code == 0:
3788 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003789 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003790 return code, out
3791
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003792 print 'Push failed with exit code %d.' % code
3793 if out.strip():
3794 print out.strip()
3795 if IsFatalPushFailure(out):
3796 print (
3797 'Fatal push error. Make sure your .netrc credentials and git '
3798 'user.email are correct and you have push access to the repo.')
3799 return code, out
3800
3801 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003802 return code, out
3803
3804
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003805def IsFatalPushFailure(push_stdout):
3806 """True if retrying push won't help."""
3807 return '(prohibited by Gerrit)' in push_stdout
3808
3809
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003810@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003811def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003812 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003813 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003814 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003815 # If it looks like previous commits were mirrored with git-svn.
3816 message = """This repository appears to be a git-svn mirror, but no
3817upstream SVN master is set. You probably need to run 'git auto-svn' once."""
3818 else:
3819 message = """This doesn't appear to be an SVN repository.
3820If your project has a true, writeable git repository, you probably want to run
3821'git cl land' instead.
3822If your project has a git mirror of an upstream SVN master, you probably need
3823to run 'git svn init'.
3824
3825Using the wrong command might cause your commit to appear to succeed, and the
3826review to be closed, without actually landing upstream. If you choose to
3827proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00003828 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00003829 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003830 return SendUpstream(parser, args, 'dcommit')
3831
3832
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003833@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003834def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003835 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003836 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003837 print('This appears to be an SVN repository.')
3838 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003839 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00003840 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003841 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003842
3843
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003844@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003845def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00003846 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003847 parser.add_option('-b', dest='newbranch',
3848 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003849 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003850 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003851 parser.add_option('-d', '--directory', action='store', metavar='DIR',
3852 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003853 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003854 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003855 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003856 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003857 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003858 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003859
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003860
3861 group = optparse.OptionGroup(
3862 parser,
3863 'Options for continuing work on the current issue uploaded from a '
3864 'different clone (e.g. different machine). Must be used independently '
3865 'from the other options. No issue number should be specified, and the '
3866 'branch must have an issue number associated with it')
3867 group.add_option('--reapply', action='store_true', dest='reapply',
3868 help='Reset the branch and reapply the issue.\n'
3869 'CAUTION: This will undo any local changes in this '
3870 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003871
3872 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003873 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003874 parser.add_option_group(group)
3875
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003876 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003877 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003878 auth_config = auth.extract_auth_config_from_options(options)
3879
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003880 cl = Changelist(auth_config=auth_config)
3881
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003882 issue_arg = None
3883 if options.reapply :
3884 if len(args) > 0:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003885 parser.error('--reapply implies no additional arguments.')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003886
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003887 issue_arg = cl.GetIssue()
3888 upstream = cl.GetUpstreamBranch()
3889 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003890 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003891
3892 RunGit(['reset', '--hard', upstream])
3893 if options.pull:
3894 RunGit(['pull'])
3895 else:
3896 if len(args) != 1:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003897 parser.error('Must specify issue number or url')
3898 issue_arg = args[0]
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003899
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003900 if not issue_arg:
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003901 parser.print_help()
3902 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003903
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003904 if cl.IsGerrit():
3905 if options.reject:
3906 parser.error('--reject is not supported with Gerrit codereview.')
3907 if options.nocommit:
3908 parser.error('--nocommit is not supported with Gerrit codereview.')
3909 if options.directory:
3910 parser.error('--directory is not supported with Gerrit codereview.')
3911
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003912 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003913 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003914 return 1
3915
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003916 if options.newbranch:
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003917 if options.reapply:
3918 parser.error("--reapply excludes any option other than --pull")
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003919 if options.force:
3920 RunGit(['branch', '-D', options.newbranch],
3921 stderr=subprocess2.PIPE, error_ok=True)
3922 RunGit(['checkout', '-b', options.newbranch,
3923 Changelist().GetUpstreamBranch()])
3924
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003925 return cl.CMDPatchIssue(issue_arg, options.reject, options.nocommit,
3926 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003927
3928
3929def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003930 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003931 # Provide a wrapper for git svn rebase to help avoid accidental
3932 # git svn dcommit.
3933 # It's the only command that doesn't use parser at all since we just defer
3934 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003935
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003936 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003937
3938
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003939def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003940 """Fetches the tree status and returns either 'open', 'closed',
3941 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003942 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003943 if url:
3944 status = urllib2.urlopen(url).read().lower()
3945 if status.find('closed') != -1 or status == '0':
3946 return 'closed'
3947 elif status.find('open') != -1 or status == '1':
3948 return 'open'
3949 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003950 return 'unset'
3951
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003952
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003953def GetTreeStatusReason():
3954 """Fetches the tree status from a json url and returns the message
3955 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00003956 url = settings.GetTreeStatusUrl()
3957 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003958 connection = urllib2.urlopen(json_url)
3959 status = json.loads(connection.read())
3960 connection.close()
3961 return status['message']
3962
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003963
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003964def GetBuilderMaster(bot_list):
3965 """For a given builder, fetch the master from AE if available."""
3966 map_url = 'https://builders-map.appspot.com/'
3967 try:
3968 master_map = json.load(urllib2.urlopen(map_url))
3969 except urllib2.URLError as e:
3970 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
3971 (map_url, e))
3972 except ValueError as e:
3973 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
3974 if not master_map:
3975 return None, 'Failed to build master map.'
3976
3977 result_master = ''
3978 for bot in bot_list:
3979 builder = bot.split(':', 1)[0]
3980 master_list = master_map.get(builder, [])
3981 if not master_list:
3982 return None, ('No matching master for builder %s.' % builder)
3983 elif len(master_list) > 1:
3984 return None, ('The builder name %s exists in multiple masters %s.' %
3985 (builder, master_list))
3986 else:
3987 cur_master = master_list[0]
3988 if not result_master:
3989 result_master = cur_master
3990 elif result_master != cur_master:
3991 return None, 'The builders do not belong to the same master.'
3992 return result_master, None
3993
3994
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003995def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003996 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003997 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003998 status = GetTreeStatus()
3999 if 'unset' == status:
4000 print 'You must configure your tree status URL by running "git cl config".'
4001 return 2
4002
4003 print "The tree is %s" % status
4004 print
4005 print GetTreeStatusReason()
4006 if status != 'open':
4007 return 1
4008 return 0
4009
4010
maruel@chromium.org15192402012-09-06 12:38:29 +00004011def CMDtry(parser, args):
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004012 """Triggers a try job through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00004013 group = optparse.OptionGroup(parser, "Try job options")
4014 group.add_option(
4015 "-b", "--bot", action="append",
4016 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
4017 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004018 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00004019 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004020 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00004021 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004022 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00004023 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004024 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004025 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004026 "-r", "--revision",
4027 help="Revision to use for the try job; default: the "
4028 "revision will be determined by the try server; see "
4029 "its waterfall for more info")
4030 group.add_option(
4031 "-c", "--clobber", action="store_true", default=False,
4032 help="Force a clobber before building; e.g. don't do an "
4033 "incremental build")
4034 group.add_option(
4035 "--project",
4036 help="Override which project to use. Projects are defined "
4037 "server-side to define what default bot set to use")
4038 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004039 "-p", "--property", dest="properties", action="append", default=[],
4040 help="Specify generic properties in the form -p key1=value1 -p "
4041 "key2=value2 etc (buildbucket only). The value will be treated as "
4042 "json if decodable, or as string otherwise.")
4043 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004044 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004045 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004046 "--use-rietveld", action="store_true", default=False,
4047 help="Use Rietveld to trigger try jobs.")
4048 group.add_option(
4049 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4050 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004051 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004052 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004053 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004054 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004055
machenbach@chromium.org45453142015-09-15 08:45:22 +00004056 if options.use_rietveld and options.properties:
4057 parser.error('Properties can only be specified with buildbucket')
4058
4059 # Make sure that all properties are prop=value pairs.
4060 bad_params = [x for x in options.properties if '=' not in x]
4061 if bad_params:
4062 parser.error('Got properties with missing "=": %s' % bad_params)
4063
maruel@chromium.org15192402012-09-06 12:38:29 +00004064 if args:
4065 parser.error('Unknown arguments: %s' % args)
4066
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004067 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004068 if not cl.GetIssue():
4069 parser.error('Need to upload first')
4070
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004071 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004072 if props.get('closed'):
4073 parser.error('Cannot send tryjobs for a closed CL')
4074
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004075 if props.get('private'):
4076 parser.error('Cannot use trybots with private issue')
4077
maruel@chromium.org15192402012-09-06 12:38:29 +00004078 if not options.name:
4079 options.name = cl.GetBranch()
4080
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004081 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004082 options.master, err_msg = GetBuilderMaster(options.bot)
4083 if err_msg:
4084 parser.error('Tryserver master cannot be found because: %s\n'
4085 'Please manually specify the tryserver master'
4086 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004087
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004088 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004089 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004090 if not options.bot:
4091 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004092
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004093 # Get try masters from PRESUBMIT.py files.
4094 masters = presubmit_support.DoGetTryMasters(
4095 change,
4096 change.LocalPaths(),
4097 settings.GetRoot(),
4098 None,
4099 None,
4100 options.verbose,
4101 sys.stdout)
4102 if masters:
4103 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004104
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004105 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4106 options.bot = presubmit_support.DoGetTrySlaves(
4107 change,
4108 change.LocalPaths(),
4109 settings.GetRoot(),
4110 None,
4111 None,
4112 options.verbose,
4113 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004114
4115 if not options.bot:
4116 # Get try masters from cq.cfg if any.
4117 # TODO(tandrii): some (but very few) projects store cq.cfg in different
4118 # location.
4119 cq_cfg = os.path.join(change.RepositoryRoot(),
4120 'infra', 'config', 'cq.cfg')
4121 if os.path.exists(cq_cfg):
4122 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00004123 cq_masters = commit_queue.get_master_builder_map(
4124 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004125 for master, builders in cq_masters.iteritems():
4126 for builder in builders:
4127 # Skip presubmit builders, because these will fail without LGTM.
4128 if 'presubmit' not in builder.lower():
4129 masters.setdefault(master, {})[builder] = ['defaulttests']
4130 if masters:
4131 return masters
4132
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004133 if not options.bot:
4134 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00004135
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004136 builders_and_tests = {}
4137 # TODO(machenbach): The old style command-line options don't support
4138 # multiple try masters yet.
4139 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4140 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4141
4142 for bot in old_style:
4143 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004144 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004145 elif ',' in bot:
4146 parser.error('Specify one bot per --bot flag')
4147 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004148 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004149
4150 for bot, tests in new_style:
4151 builders_and_tests.setdefault(bot, []).extend(tests)
4152
4153 # Return a master map with one master to be backwards compatible. The
4154 # master name defaults to an empty string, which will cause the master
4155 # not to be set on rietveld (deprecated).
4156 return {options.master: builders_and_tests}
4157
4158 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004159
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004160 for builders in masters.itervalues():
4161 if any('triggered' in b for b in builders):
4162 print >> sys.stderr, (
4163 'ERROR You are trying to send a job to a triggered bot. This type of'
4164 ' bot requires an\ninitial job from a parent (usually a builder). '
4165 'Instead send your job to the parent.\n'
4166 'Bot list: %s' % builders)
4167 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004168
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004169 patchset = cl.GetMostRecentPatchset()
4170 if patchset and patchset != cl.GetPatchset():
4171 print(
4172 '\nWARNING Mismatch between local config and server. Did a previous '
4173 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4174 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004175 if options.luci:
4176 trigger_luci_job(cl, masters, options)
4177 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004178 try:
4179 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4180 except BuildbucketResponseException as ex:
4181 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004182 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004183 except Exception as e:
4184 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4185 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
4186 e, stacktrace)
4187 return 1
4188 else:
4189 try:
4190 cl.RpcServer().trigger_distributed_try_jobs(
4191 cl.GetIssue(), patchset, options.name, options.clobber,
4192 options.revision, masters)
4193 except urllib2.HTTPError as e:
4194 if e.code == 404:
4195 print('404 from rietveld; '
4196 'did you mean to use "git try" instead of "git cl try"?')
4197 return 1
4198 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004199
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004200 for (master, builders) in sorted(masters.iteritems()):
4201 if master:
4202 print 'Master: %s' % master
4203 length = max(len(builder) for builder in builders)
4204 for builder in sorted(builders):
4205 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00004206 return 0
4207
4208
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004209def CMDtry_results(parser, args):
4210 group = optparse.OptionGroup(parser, "Try job results options")
4211 group.add_option(
4212 "-p", "--patchset", type=int, help="patchset number if not current.")
4213 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004214 "--print-master", action='store_true', help="print master name as well.")
4215 group.add_option(
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004216 "--color", action='store_true', default=setup_color.IS_TTY,
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004217 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004218 group.add_option(
4219 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4220 help="Host of buildbucket. The default host is %default.")
4221 parser.add_option_group(group)
4222 auth.add_auth_options(parser)
4223 options, args = parser.parse_args(args)
4224 if args:
4225 parser.error('Unrecognized args: %s' % ' '.join(args))
4226
4227 auth_config = auth.extract_auth_config_from_options(options)
4228 cl = Changelist(auth_config=auth_config)
4229 if not cl.GetIssue():
4230 parser.error('Need to upload first')
4231
4232 if not options.patchset:
4233 options.patchset = cl.GetMostRecentPatchset()
4234 if options.patchset and options.patchset != cl.GetPatchset():
4235 print(
4236 '\nWARNING Mismatch between local config and server. Did a previous '
4237 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4238 'Continuing using\npatchset %s.\n' % options.patchset)
4239 try:
4240 jobs = fetch_try_jobs(auth_config, cl, options)
4241 except BuildbucketResponseException as ex:
4242 print 'Buildbucket error: %s' % ex
4243 return 1
4244 except Exception as e:
4245 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4246 print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
4247 e, stacktrace)
4248 return 1
4249 print_tryjobs(options, jobs)
4250 return 0
4251
4252
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004253@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004254def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004255 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004256 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004257 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004258 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004259
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004260 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004261 if args:
4262 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004263 branch = cl.GetBranch()
4264 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004265 cl = Changelist()
4266 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004267
4268 # Clear configured merge-base, if there is one.
4269 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004270 else:
4271 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004272 return 0
4273
4274
thestig@chromium.org00858c82013-12-02 23:08:03 +00004275def CMDweb(parser, args):
4276 """Opens the current CL in the web browser."""
4277 _, args = parser.parse_args(args)
4278 if args:
4279 parser.error('Unrecognized args: %s' % ' '.join(args))
4280
4281 issue_url = Changelist().GetIssueURL()
4282 if not issue_url:
4283 print >> sys.stderr, 'ERROR No issue to open'
4284 return 1
4285
4286 webbrowser.open(issue_url)
4287 return 0
4288
4289
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004290def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004291 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004292 auth.add_auth_options(parser)
4293 options, args = parser.parse_args(args)
4294 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004295 if args:
4296 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004297 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004298 props = cl.GetIssueProperties()
4299 if props.get('private'):
4300 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004301 cl.SetFlag('commit', '1')
4302 return 0
4303
4304
groby@chromium.org411034a2013-02-26 15:12:01 +00004305def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004306 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004307 auth.add_auth_options(parser)
4308 options, args = parser.parse_args(args)
4309 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004310 if args:
4311 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004312 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00004313 # Ensure there actually is an issue to close.
4314 cl.GetDescription()
4315 cl.CloseIssue()
4316 return 0
4317
4318
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004319def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004320 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004321 auth.add_auth_options(parser)
4322 options, args = parser.parse_args(args)
4323 auth_config = auth.extract_auth_config_from_options(options)
4324 if args:
4325 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004326
4327 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004328 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004329 # Staged changes would be committed along with the patch from last
4330 # upload, hence counted toward the "last upload" side in the final
4331 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004332 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004333 return 1
4334
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004335 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004336 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004337 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004338 if not issue:
4339 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004340 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004341 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004342
4343 # Create a new branch based on the merge-base
4344 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004345 # Clear cached branch in cl object, to avoid overwriting original CL branch
4346 # properties.
4347 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004348 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004349 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004350 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004351 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004352 return rtn
4353
wychen@chromium.org06928532015-02-03 02:11:29 +00004354 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004355 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00004356 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004357 finally:
4358 RunGit(['checkout', '-q', branch])
4359 RunGit(['branch', '-D', TMP_BRANCH])
4360
4361 return 0
4362
4363
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004364def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004365 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004366 parser.add_option(
4367 '--no-color',
4368 action='store_true',
4369 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004370 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004371 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004372 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004373
4374 author = RunGit(['config', 'user.email']).strip() or None
4375
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004376 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004377
4378 if args:
4379 if len(args) > 1:
4380 parser.error('Unknown args')
4381 base_branch = args[0]
4382 else:
4383 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004384 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004385
4386 change = cl.GetChange(base_branch, None)
4387 return owners_finder.OwnersFinder(
4388 [f.LocalPath() for f in
4389 cl.GetChange(base_branch, None).AffectedFiles()],
4390 change.RepositoryRoot(), author,
4391 fopen=file, os_path=os.path, glob=glob.glob,
4392 disable_color=options.no_color).run()
4393
4394
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004395def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004396 """Generates a diff command."""
4397 # Generate diff for the current branch's changes.
4398 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4399 upstream_commit, '--' ]
4400
4401 if args:
4402 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004403 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004404 diff_cmd.append(arg)
4405 else:
4406 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004407
4408 return diff_cmd
4409
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004410def MatchingFileType(file_name, extensions):
4411 """Returns true if the file name ends with one of the given extensions."""
4412 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004413
enne@chromium.org555cfe42014-01-29 18:21:39 +00004414@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004415def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004416 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00004417 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004418 GN_EXTS = ['.gn', '.gni']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004419 parser.add_option('--full', action='store_true',
4420 help='Reformat the full content of all touched files')
4421 parser.add_option('--dry-run', action='store_true',
4422 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004423 parser.add_option('--python', action='store_true',
4424 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004425 parser.add_option('--diff', action='store_true',
4426 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004427 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004428
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004429 # git diff generates paths against the root of the repository. Change
4430 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004431 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004432 if rel_base_path:
4433 os.chdir(rel_base_path)
4434
digit@chromium.org29e47272013-05-17 17:01:46 +00004435 # Grab the merge-base commit, i.e. the upstream commit of the current
4436 # branch when it was created or the last time it was rebased. This is
4437 # to cover the case where the user may have called "git fetch origin",
4438 # moving the origin branch to a newer commit, but hasn't rebased yet.
4439 upstream_commit = None
4440 cl = Changelist()
4441 upstream_branch = cl.GetUpstreamBranch()
4442 if upstream_branch:
4443 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4444 upstream_commit = upstream_commit.strip()
4445
4446 if not upstream_commit:
4447 DieWithError('Could not find base commit for this branch. '
4448 'Are you in detached state?')
4449
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004450 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4451 diff_output = RunGit(changed_files_cmd)
4452 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004453 # Filter out files deleted by this CL
4454 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004455
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004456 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4457 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4458 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004459 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004460
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004461 top_dir = os.path.normpath(
4462 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
4463
4464 # Locate the clang-format binary in the checkout
4465 try:
4466 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
4467 except clang_format.NotFoundError, e:
4468 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00004469
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004470 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4471 # formatted. This is used to block during the presubmit.
4472 return_value = 0
4473
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004474 if clang_diff_files:
4475 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004476 cmd = [clang_format_tool]
4477 if not opts.dry_run and not opts.diff:
4478 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004479 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004480 if opts.diff:
4481 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004482 else:
4483 env = os.environ.copy()
4484 env['PATH'] = str(os.path.dirname(clang_format_tool))
4485 try:
4486 script = clang_format.FindClangFormatScriptInChromiumTree(
4487 'clang-format-diff.py')
4488 except clang_format.NotFoundError, e:
4489 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004490
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004491 cmd = [sys.executable, script, '-p0']
4492 if not opts.dry_run and not opts.diff:
4493 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004494
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004495 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
4496 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004497
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004498 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
4499 if opts.diff:
4500 sys.stdout.write(stdout)
4501 if opts.dry_run and len(stdout) > 0:
4502 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004503
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004504 # Similar code to above, but using yapf on .py files rather than clang-format
4505 # on C/C++ files
4506 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004507 yapf_tool = gclient_utils.FindExecutable('yapf')
4508 if yapf_tool is None:
4509 DieWithError('yapf not found in PATH')
4510
4511 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004512 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004513 cmd = [yapf_tool]
4514 if not opts.dry_run and not opts.diff:
4515 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004516 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004517 if opts.diff:
4518 sys.stdout.write(stdout)
4519 else:
4520 # TODO(sbc): yapf --lines mode still has some issues.
4521 # https://github.com/google/yapf/issues/154
4522 DieWithError('--python currently only works with --full')
4523
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004524 # Dart's formatter does not have the nice property of only operating on
4525 # modified chunks, so hard code full.
4526 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004527 try:
4528 command = [dart_format.FindDartFmtToolInChromiumTree()]
4529 if not opts.dry_run and not opts.diff:
4530 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004531 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004532
ppi@chromium.org6593d932016-03-03 15:41:15 +00004533 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004534 if opts.dry_run and stdout:
4535 return_value = 2
4536 except dart_format.NotFoundError as e:
erikcorry@chromium.org3e445022015-12-17 09:07:26 +00004537 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
4538 'found in this checkout. Files in other languages are still ' +
4539 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004540
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004541 # Format GN build files. Always run on full build files for canonical form.
4542 if gn_diff_files:
4543 cmd = ['gn', 'format']
4544 if not opts.dry_run and not opts.diff:
4545 cmd.append('--in-place')
4546 for gn_diff_file in gn_diff_files:
4547 stdout = RunCommand(cmd + [gn_diff_file], cwd=top_dir)
4548 if opts.diff:
4549 sys.stdout.write(stdout)
4550
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004551 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004552
4553
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004554@subcommand.usage('<codereview url or issue id>')
4555def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004556 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004557 _, args = parser.parse_args(args)
4558
4559 if len(args) != 1:
4560 parser.print_help()
4561 return 1
4562
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004563 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00004564 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004565 parser.print_help()
4566 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00004567 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004568
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004569 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00004570 output = RunGit(['config', '--local', '--get-regexp',
4571 r'branch\..*\.%s' % issueprefix],
4572 error_ok=True)
4573 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004574 if issue == target_issue:
4575 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004576
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004577 branches = []
4578 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00004579 branches.extend(find_issues(cls.IssueSettingSuffix()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004580 if len(branches) == 0:
4581 print 'No branch found for issue %s.' % target_issue
4582 return 1
4583 if len(branches) == 1:
4584 RunGit(['checkout', branches[0]])
4585 else:
4586 print 'Multiple branches match issue %s:' % target_issue
4587 for i in range(len(branches)):
4588 print '%d: %s' % (i, branches[i])
4589 which = raw_input('Choose by index: ')
4590 try:
4591 RunGit(['checkout', branches[int(which)]])
4592 except (IndexError, ValueError):
4593 print 'Invalid selection, not checking out any branch.'
4594 return 1
4595
4596 return 0
4597
4598
maruel@chromium.org29404b52014-09-08 22:58:00 +00004599def CMDlol(parser, args):
4600 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00004601 print zlib.decompress(base64.b64decode(
4602 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
4603 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
4604 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
4605 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00004606 return 0
4607
4608
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004609class OptionParser(optparse.OptionParser):
4610 """Creates the option parse and add --verbose support."""
4611 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004612 optparse.OptionParser.__init__(
4613 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004614 self.add_option(
4615 '-v', '--verbose', action='count', default=0,
4616 help='Use 2 times for more debugging info')
4617
4618 def parse_args(self, args=None, values=None):
4619 options, args = optparse.OptionParser.parse_args(self, args, values)
4620 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
4621 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
4622 return options, args
4623
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004624
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004625def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00004626 if sys.hexversion < 0x02060000:
4627 print >> sys.stderr, (
4628 '\nYour python version %s is unsupported, please upgrade.\n' %
4629 sys.version.split(' ', 1)[0])
4630 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004631
maruel@chromium.orgddd59412011-11-30 14:20:38 +00004632 # Reload settings.
4633 global settings
4634 settings = Settings()
4635
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004636 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004637 dispatcher = subcommand.CommandDispatcher(__name__)
4638 try:
4639 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00004640 except auth.AuthenticationError as e:
4641 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004642 except urllib2.HTTPError, e:
4643 if e.code != 500:
4644 raise
4645 DieWithError(
4646 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
4647 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00004648 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004649
4650
4651if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004652 # These affect sys.stdout so do it outside of main() to simplify mocks in
4653 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00004654 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004655 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00004656 try:
4657 sys.exit(main(sys.argv[1:]))
4658 except KeyboardInterrupt:
4659 sys.stderr.write('interrupted\n')
4660 sys.exit(1)