blob: 8f71fd598abf0b7600b78ec0b5c0a63f8ce5e174 [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
maruel@chromium.org725f1c32011-04-01 20:24:54 +00008"""A git-command for integrating reviews on Rietveld."""
9
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
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000048import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000049import gclient_utils
iannucci@chromium.org9e849272014-04-04 00:31:55 +000050import git_common
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +000051from git_footers import get_footer_svn_id
piman@chromium.org336f9122014-09-04 02:16:55 +000052import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000053import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000054import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000055import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000056import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000057import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000058import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000059import watchlists
60
maruel@chromium.org0633fb42013-08-16 20:06:14 +000061__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000062
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000063DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000064POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000065DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000066GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000067CHANGE_ID = 'Change-Id:'
rmistry@google.comc68112d2015-03-03 12:48:06 +000068REFS_THAT_ALIAS_TO_OTHER_REFS = {
69 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
70 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
71}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000072
thestig@chromium.org44202a22014-03-11 19:22:18 +000073# Valid extensions for files we want to lint.
74DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
75DEFAULT_LINT_IGNORE_REGEX = r"$^"
76
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000077# Shortcut since it quickly becomes redundant.
78Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000079
maruel@chromium.orgddd59412011-11-30 14:20:38 +000080# Initialized in main()
81settings = None
82
83
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000084def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000085 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000086 sys.exit(1)
87
88
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000089def GetNoGitPagerEnv():
90 env = os.environ.copy()
91 # 'cat' is a magical git string that disables pagers on all platforms.
92 env['GIT_PAGER'] = 'cat'
93 return env
94
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000095
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000096def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000097 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000098 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000099 except subprocess2.CalledProcessError as e:
100 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000101 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000102 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000103 'Command "%s" failed.\n%s' % (
104 ' '.join(args), error_message or e.stdout or ''))
105 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000106
107
108def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000109 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000110 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000111
112
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000113def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000114 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000115 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000116 if suppress_stderr:
117 stderr = subprocess2.VOID
118 else:
119 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000120 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000121 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000122 stdout=subprocess2.PIPE,
123 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000124 return code, out[0]
125 except ValueError:
126 # When the subprocess fails, it returns None. That triggers a ValueError
127 # when trying to unpack the return value into (out, code).
128 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000129
130
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000131def RunGitSilent(args):
132 """Returns stdout, suppresses stderr and ingores the return code."""
133 return RunGitWithCode(args, suppress_stderr=True)[1]
134
135
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000136def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000137 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000138 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000139 return (version.startswith(prefix) and
140 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000141
142
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000143def BranchExists(branch):
144 """Return True if specified branch exists."""
145 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
146 suppress_stderr=True)
147 return not code
148
149
maruel@chromium.org90541732011-04-01 17:54:18 +0000150def ask_for_data(prompt):
151 try:
152 return raw_input(prompt)
153 except KeyboardInterrupt:
154 # Hide the exception.
155 sys.exit(1)
156
157
iannucci@chromium.org79540052012-10-19 23:15:26 +0000158def git_set_branch_value(key, value):
159 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000160 if not branch:
161 return
162
163 cmd = ['config']
164 if isinstance(value, int):
165 cmd.append('--int')
166 git_key = 'branch.%s.%s' % (branch, key)
167 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000168
169
170def git_get_branch_default(key, default):
171 branch = Changelist().GetBranch()
172 if branch:
173 git_key = 'branch.%s.%s' % (branch, key)
174 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
175 try:
176 return int(stdout.strip())
177 except ValueError:
178 pass
179 return default
180
181
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000182def add_git_similarity(parser):
183 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000184 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000185 help='Sets the percentage that a pair of files need to match in order to'
186 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000187 parser.add_option(
188 '--find-copies', action='store_true',
189 help='Allows git to look for copies.')
190 parser.add_option(
191 '--no-find-copies', action='store_false', dest='find_copies',
192 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000193
194 old_parser_args = parser.parse_args
195 def Parse(args):
196 options, args = old_parser_args(args)
197
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000198 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000199 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000200 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000201 print('Note: Saving similarity of %d%% in git config.'
202 % options.similarity)
203 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000204
iannucci@chromium.org79540052012-10-19 23:15:26 +0000205 options.similarity = max(0, min(options.similarity, 100))
206
207 if options.find_copies is None:
208 options.find_copies = bool(
209 git_get_branch_default('git-find-copies', True))
210 else:
211 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000212
213 print('Using %d%% similarity for rename/copy detection. '
214 'Override with --similarity.' % options.similarity)
215
216 return options, args
217 parser.parse_args = Parse
218
219
machenbach@chromium.org45453142015-09-15 08:45:22 +0000220def _get_properties_from_options(options):
221 properties = dict(x.split('=', 1) for x in options.properties)
222 for key, val in properties.iteritems():
223 try:
224 properties[key] = json.loads(val)
225 except ValueError:
226 pass # If a value couldn't be evaluated, treat it as a string.
227 return properties
228
229
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000230def _prefix_master(master):
231 """Convert user-specified master name to full master name.
232
233 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
234 name, while the developers always use shortened master name
235 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
236 function does the conversion for buildbucket migration.
237 """
238 prefix = 'master.'
239 if master.startswith(prefix):
240 return master
241 return '%s%s' % (prefix, master)
242
243
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000244def _buildbucket_retry(operation_name, http, *args, **kwargs):
245 """Retries requests to buildbucket service and returns parsed json content."""
246 try_count = 0
247 while True:
248 response, content = http.request(*args, **kwargs)
249 try:
250 content_json = json.loads(content)
251 except ValueError:
252 content_json = None
253
254 # Buildbucket could return an error even if status==200.
255 if content_json and content_json.get('error'):
256 msg = 'Error in response. Reason: %s. Message: %s.' % (
257 content_json['error'].get('reason', ''),
258 content_json['error'].get('message', ''))
259 raise BuildbucketResponseException(msg)
260
261 if response.status == 200:
262 if not content_json:
263 raise BuildbucketResponseException(
264 'Buildbucket returns invalid json content: %s.\n'
265 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
266 content)
267 return content_json
268 if response.status < 500 or try_count >= 2:
269 raise httplib2.HttpLib2Error(content)
270
271 # status >= 500 means transient failures.
272 logging.debug('Transient errors when %s. Will retry.', operation_name)
273 time.sleep(0.5 + 1.5*try_count)
274 try_count += 1
275 assert False, 'unreachable'
276
277
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000278def trigger_luci_job(changelist, masters, options):
279 """Send a job to run on LUCI."""
280 issue_props = changelist.GetIssueProperties()
281 issue = changelist.GetIssue()
282 patchset = changelist.GetMostRecentPatchset()
283 for builders_and_tests in sorted(masters.itervalues()):
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000284 # TODO(hinoka et al): add support for other properties.
285 # Currently, this completely ignores testfilter and other properties.
286 for builder in sorted(builders_and_tests):
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000287 luci_trigger.trigger(
288 builder, 'HEAD', issue, patchset, issue_props['project'])
289
290
machenbach@chromium.org45453142015-09-15 08:45:22 +0000291def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000292 rietveld_url = settings.GetDefaultServerUrl()
293 rietveld_host = urlparse.urlparse(rietveld_url).hostname
294 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
295 http = authenticator.authorize(httplib2.Http())
296 http.force_exception_to_status_code = True
297 issue_props = changelist.GetIssueProperties()
298 issue = changelist.GetIssue()
299 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000300 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000301
302 buildbucket_put_url = (
303 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000304 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000305 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
306 hostname=rietveld_host,
307 issue=issue,
308 patch=patchset)
309
310 batch_req_body = {'builds': []}
311 print_text = []
312 print_text.append('Tried jobs on:')
313 for master, builders_and_tests in sorted(masters.iteritems()):
314 print_text.append('Master: %s' % master)
315 bucket = _prefix_master(master)
316 for builder, tests in sorted(builders_and_tests.iteritems()):
317 print_text.append(' %s: %s' % (builder, tests))
318 parameters = {
319 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000320 'changes': [{
321 'author': {'email': issue_props['owner_email']},
322 'revision': options.revision,
323 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000324 'properties': {
325 'category': category,
326 'issue': issue,
327 'master': master,
328 'patch_project': issue_props['project'],
329 'patch_storage': 'rietveld',
330 'patchset': patchset,
331 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000332 'rietveld': rietveld_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000333 },
334 }
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000335 if tests:
336 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000337 if properties:
338 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000339 if options.clobber:
340 parameters['properties']['clobber'] = True
341 batch_req_body['builds'].append(
342 {
343 'bucket': bucket,
344 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000345 'client_operation_id': str(uuid.uuid4()),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000346 'tags': ['builder:%s' % builder,
347 'buildset:%s' % buildset,
348 'master:%s' % master,
349 'user_agent:git_cl_try']
350 }
351 )
352
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000353 _buildbucket_retry(
354 'triggering tryjobs',
355 http,
356 buildbucket_put_url,
357 'PUT',
358 body=json.dumps(batch_req_body),
359 headers={'Content-Type': 'application/json'}
360 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000361 print_text.append('To see results here, run: git cl try-results')
362 print_text.append('To see results in browser, run: git cl web')
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000363 print '\n'.join(print_text)
kjellander@chromium.org44424542015-06-02 18:35:29 +0000364
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000365
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000366def fetch_try_jobs(auth_config, changelist, options):
367 """Fetches tryjobs from buildbucket.
368
369 Returns a map from build id to build info as json dictionary.
370 """
371 rietveld_url = settings.GetDefaultServerUrl()
372 rietveld_host = urlparse.urlparse(rietveld_url).hostname
373 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
374 if authenticator.has_cached_credentials():
375 http = authenticator.authorize(httplib2.Http())
376 else:
377 print ('Warning: Some results might be missing because %s' %
378 # Get the message on how to login.
379 auth.LoginRequiredError(rietveld_host).message)
380 http = httplib2.Http()
381
382 http.force_exception_to_status_code = True
383
384 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
385 hostname=rietveld_host,
386 issue=changelist.GetIssue(),
387 patch=options.patchset)
388 params = {'tag': 'buildset:%s' % buildset}
389
390 builds = {}
391 while True:
392 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
393 hostname=options.buildbucket_host,
394 params=urllib.urlencode(params))
395 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
396 for build in content.get('builds', []):
397 builds[build['id']] = build
398 if 'next_cursor' in content:
399 params['start_cursor'] = content['next_cursor']
400 else:
401 break
402 return builds
403
404
405def print_tryjobs(options, builds):
406 """Prints nicely result of fetch_try_jobs."""
407 if not builds:
408 print 'No tryjobs scheduled'
409 return
410
411 # Make a copy, because we'll be modifying builds dictionary.
412 builds = builds.copy()
413 builder_names_cache = {}
414
415 def get_builder(b):
416 try:
417 return builder_names_cache[b['id']]
418 except KeyError:
419 try:
420 parameters = json.loads(b['parameters_json'])
421 name = parameters['builder_name']
422 except (ValueError, KeyError) as error:
423 print 'WARNING: failed to get builder name for build %s: %s' % (
424 b['id'], error)
425 name = None
426 builder_names_cache[b['id']] = name
427 return name
428
429 def get_bucket(b):
430 bucket = b['bucket']
431 if bucket.startswith('master.'):
432 return bucket[len('master.'):]
433 return bucket
434
435 if options.print_master:
436 name_fmt = '%%-%ds %%-%ds' % (
437 max(len(str(get_bucket(b))) for b in builds.itervalues()),
438 max(len(str(get_builder(b))) for b in builds.itervalues()))
439 def get_name(b):
440 return name_fmt % (get_bucket(b), get_builder(b))
441 else:
442 name_fmt = '%%-%ds' % (
443 max(len(str(get_builder(b))) for b in builds.itervalues()))
444 def get_name(b):
445 return name_fmt % get_builder(b)
446
447 def sort_key(b):
448 return b['status'], b.get('result'), get_name(b), b.get('url')
449
450 def pop(title, f, color=None, **kwargs):
451 """Pop matching builds from `builds` dict and print them."""
452
453 if not sys.stdout.isatty() or color is None:
454 colorize = str
455 else:
456 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
457
458 result = []
459 for b in builds.values():
460 if all(b.get(k) == v for k, v in kwargs.iteritems()):
461 builds.pop(b['id'])
462 result.append(b)
463 if result:
464 print colorize(title)
465 for b in sorted(result, key=sort_key):
466 print ' ', colorize('\t'.join(map(str, f(b))))
467
468 total = len(builds)
469 pop(status='COMPLETED', result='SUCCESS',
470 title='Successes:', color=Fore.GREEN,
471 f=lambda b: (get_name(b), b.get('url')))
472 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
473 title='Infra Failures:', color=Fore.MAGENTA,
474 f=lambda b: (get_name(b), b.get('url')))
475 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
476 title='Failures:', color=Fore.RED,
477 f=lambda b: (get_name(b), b.get('url')))
478 pop(status='COMPLETED', result='CANCELED',
479 title='Canceled:', color=Fore.MAGENTA,
480 f=lambda b: (get_name(b),))
481 pop(status='COMPLETED', result='FAILURE',
482 failure_reason='INVALID_BUILD_DEFINITION',
483 title='Wrong master/builder name:', color=Fore.MAGENTA,
484 f=lambda b: (get_name(b),))
485 pop(status='COMPLETED', result='FAILURE',
486 title='Other failures:',
487 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
488 pop(status='COMPLETED',
489 title='Other finished:',
490 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
491 pop(status='STARTED',
492 title='Started:', color=Fore.YELLOW,
493 f=lambda b: (get_name(b), b.get('url')))
494 pop(status='SCHEDULED',
495 title='Scheduled:',
496 f=lambda b: (get_name(b), 'id=%s' % b['id']))
497 # The last section is just in case buildbucket API changes OR there is a bug.
498 pop(title='Other:',
499 f=lambda b: (get_name(b), 'id=%s' % b['id']))
500 assert len(builds) == 0
501 print 'Total: %d tryjobs' % total
502
503
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000504def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
505 """Return the corresponding git ref if |base_url| together with |glob_spec|
506 matches the full |url|.
507
508 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
509 """
510 fetch_suburl, as_ref = glob_spec.split(':')
511 if allow_wildcards:
512 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
513 if glob_match:
514 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
515 # "branches/{472,597,648}/src:refs/remotes/svn/*".
516 branch_re = re.escape(base_url)
517 if glob_match.group(1):
518 branch_re += '/' + re.escape(glob_match.group(1))
519 wildcard = glob_match.group(2)
520 if wildcard == '*':
521 branch_re += '([^/]*)'
522 else:
523 # Escape and replace surrounding braces with parentheses and commas
524 # with pipe symbols.
525 wildcard = re.escape(wildcard)
526 wildcard = re.sub('^\\\\{', '(', wildcard)
527 wildcard = re.sub('\\\\,', '|', wildcard)
528 wildcard = re.sub('\\\\}$', ')', wildcard)
529 branch_re += wildcard
530 if glob_match.group(3):
531 branch_re += re.escape(glob_match.group(3))
532 match = re.match(branch_re, url)
533 if match:
534 return re.sub('\*$', match.group(1), as_ref)
535
536 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
537 if fetch_suburl:
538 full_url = base_url + '/' + fetch_suburl
539 else:
540 full_url = base_url
541 if full_url == url:
542 return as_ref
543 return None
544
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000545
iannucci@chromium.org79540052012-10-19 23:15:26 +0000546def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000547 """Prints statistics about the change to the user."""
548 # --no-ext-diff is broken in some versions of Git, so try to work around
549 # this by overriding the environment (but there is still a problem if the
550 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000551 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000552 if 'GIT_EXTERNAL_DIFF' in env:
553 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000554
555 if find_copies:
556 similarity_options = ['--find-copies-harder', '-l100000',
557 '-C%s' % similarity]
558 else:
559 similarity_options = ['-M%s' % similarity]
560
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000561 try:
562 stdout = sys.stdout.fileno()
563 except AttributeError:
564 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000565 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000566 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000567 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000568 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000569
570
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000571class BuildbucketResponseException(Exception):
572 pass
573
574
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000575class Settings(object):
576 def __init__(self):
577 self.default_server = None
578 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000579 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000580 self.is_git_svn = None
581 self.svn_branch = None
582 self.tree_status_url = None
583 self.viewvc_url = None
584 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000585 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000586 self.squash_gerrit_uploads = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000587 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000588 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000589 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000590 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000591
592 def LazyUpdateIfNeeded(self):
593 """Updates the settings from a codereview.settings file, if available."""
594 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000595 # The only value that actually changes the behavior is
596 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000597 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000598 error_ok=True
599 ).strip().lower()
600
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000601 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000602 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000603 LoadCodereviewSettingsFromFile(cr_settings_file)
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000604 # set updated to True to avoid infinite calling loop
605 # through DownloadHooks
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000606 self.updated = True
607 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000608 self.updated = True
609
610 def GetDefaultServerUrl(self, error_ok=False):
611 if not self.default_server:
612 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000613 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000614 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000615 if error_ok:
616 return self.default_server
617 if not self.default_server:
618 error_message = ('Could not find settings file. You must configure '
619 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000620 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000621 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000622 return self.default_server
623
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000624 @staticmethod
625 def GetRelativeRoot():
626 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000627
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000628 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000629 if self.root is None:
630 self.root = os.path.abspath(self.GetRelativeRoot())
631 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000632
633 def GetIsGitSvn(self):
634 """Return true if this repo looks like it's using git-svn."""
635 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000636 if self.GetPendingRefPrefix():
637 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
638 self.is_git_svn = False
639 else:
640 # If you have any "svn-remote.*" config keys, we think you're using svn.
641 self.is_git_svn = RunGitWithCode(
642 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000643 return self.is_git_svn
644
645 def GetSVNBranch(self):
646 if self.svn_branch is None:
647 if not self.GetIsGitSvn():
648 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
649
650 # Try to figure out which remote branch we're based on.
651 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000652 # 1) iterate through our branch history and find the svn URL.
653 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000654
655 # regexp matching the git-svn line that contains the URL.
656 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
657
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000658 # We don't want to go through all of history, so read a line from the
659 # pipe at a time.
660 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000661 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000662 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
663 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000664 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000665 for line in proc.stdout:
666 match = git_svn_re.match(line)
667 if match:
668 url = match.group(1)
669 proc.stdout.close() # Cut pipe.
670 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000671
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000672 if url:
673 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
674 remotes = RunGit(['config', '--get-regexp',
675 r'^svn-remote\..*\.url']).splitlines()
676 for remote in remotes:
677 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000678 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000679 remote = match.group(1)
680 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000681 rewrite_root = RunGit(
682 ['config', 'svn-remote.%s.rewriteRoot' % remote],
683 error_ok=True).strip()
684 if rewrite_root:
685 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000686 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000687 ['config', 'svn-remote.%s.fetch' % remote],
688 error_ok=True).strip()
689 if fetch_spec:
690 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
691 if self.svn_branch:
692 break
693 branch_spec = RunGit(
694 ['config', 'svn-remote.%s.branches' % remote],
695 error_ok=True).strip()
696 if branch_spec:
697 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
698 if self.svn_branch:
699 break
700 tag_spec = RunGit(
701 ['config', 'svn-remote.%s.tags' % remote],
702 error_ok=True).strip()
703 if tag_spec:
704 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
705 if self.svn_branch:
706 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000707
708 if not self.svn_branch:
709 DieWithError('Can\'t guess svn branch -- try specifying it on the '
710 'command line')
711
712 return self.svn_branch
713
714 def GetTreeStatusUrl(self, error_ok=False):
715 if not self.tree_status_url:
716 error_message = ('You must configure your tree status URL by running '
717 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000718 self.tree_status_url = self._GetRietveldConfig(
719 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000720 return self.tree_status_url
721
722 def GetViewVCUrl(self):
723 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000724 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000725 return self.viewvc_url
726
rmistry@google.com90752582014-01-14 21:04:50 +0000727 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000728 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000729
rmistry@google.com78948ed2015-07-08 23:09:57 +0000730 def GetIsSkipDependencyUpload(self, branch_name):
731 """Returns true if specified branch should skip dep uploads."""
732 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
733 error_ok=True)
734
rmistry@google.com5626a922015-02-26 14:03:30 +0000735 def GetRunPostUploadHook(self):
736 run_post_upload_hook = self._GetRietveldConfig(
737 'run-post-upload-hook', error_ok=True)
738 return run_post_upload_hook == "True"
739
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000740 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000741 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000742
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000743 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000744 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000745
ukai@chromium.orge8077812012-02-03 03:41:46 +0000746 def GetIsGerrit(self):
747 """Return true if this repo is assosiated with gerrit code review system."""
748 if self.is_gerrit is None:
749 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
750 return self.is_gerrit
751
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000752 def GetSquashGerritUploads(self):
753 """Return true if uploads to Gerrit should be squashed by default."""
754 if self.squash_gerrit_uploads is None:
755 self.squash_gerrit_uploads = (
756 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
757 error_ok=True).strip() == 'true')
758 return self.squash_gerrit_uploads
759
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000760 def GetGitEditor(self):
761 """Return the editor specified in the git config, or None if none is."""
762 if self.git_editor is None:
763 self.git_editor = self._GetConfig('core.editor', error_ok=True)
764 return self.git_editor or None
765
thestig@chromium.org44202a22014-03-11 19:22:18 +0000766 def GetLintRegex(self):
767 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
768 DEFAULT_LINT_REGEX)
769
770 def GetLintIgnoreRegex(self):
771 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
772 DEFAULT_LINT_IGNORE_REGEX)
773
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000774 def GetProject(self):
775 if not self.project:
776 self.project = self._GetRietveldConfig('project', error_ok=True)
777 return self.project
778
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000779 def GetForceHttpsCommitUrl(self):
780 if not self.force_https_commit_url:
781 self.force_https_commit_url = self._GetRietveldConfig(
782 'force-https-commit-url', error_ok=True)
783 return self.force_https_commit_url
784
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000785 def GetPendingRefPrefix(self):
786 if not self.pending_ref_prefix:
787 self.pending_ref_prefix = self._GetRietveldConfig(
788 'pending-ref-prefix', error_ok=True)
789 return self.pending_ref_prefix
790
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000791 def _GetRietveldConfig(self, param, **kwargs):
792 return self._GetConfig('rietveld.' + param, **kwargs)
793
rmistry@google.com78948ed2015-07-08 23:09:57 +0000794 def _GetBranchConfig(self, branch_name, param, **kwargs):
795 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
796
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000797 def _GetConfig(self, param, **kwargs):
798 self.LazyUpdateIfNeeded()
799 return RunGit(['config', param], **kwargs).strip()
800
801
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000802def ShortBranchName(branch):
803 """Convert a name like 'refs/heads/foo' to just 'foo'."""
804 return branch.replace('refs/heads/', '')
805
806
807class Changelist(object):
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000808 def __init__(self, branchref=None, issue=None, auth_config=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000809 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000810 global settings
811 if not settings:
812 # Happens when git_cl.py is used as a utility library.
813 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000814 settings.GetDefaultServerUrl()
815 self.branchref = branchref
816 if self.branchref:
817 self.branch = ShortBranchName(self.branchref)
818 else:
819 self.branch = None
820 self.rietveld_server = None
821 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000822 self.lookedup_issue = False
823 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000824 self.has_description = False
825 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000826 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000827 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000828 self.cc = None
829 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000830 self._auth_config = auth_config
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000831 self._props = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000832 self._remote = None
833 self._rpc_server = None
834
835 @property
836 def auth_config(self):
837 return self._auth_config
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000838
839 def GetCCList(self):
840 """Return the users cc'd on this CL.
841
842 Return is a string suitable for passing to gcl with the --cc flag.
843 """
844 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000845 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000846 more_cc = ','.join(self.watchers)
847 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
848 return self.cc
849
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000850 def GetCCListWithoutDefault(self):
851 """Return the users cc'd on this CL excluding default ones."""
852 if self.cc is None:
853 self.cc = ','.join(self.watchers)
854 return self.cc
855
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000856 def SetWatchers(self, watchers):
857 """Set the list of email addresses that should be cc'd based on the changed
858 files in this CL.
859 """
860 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000861
862 def GetBranch(self):
863 """Returns the short branch name, e.g. 'master'."""
864 if not self.branch:
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000865 branchref = RunGit(['symbolic-ref', 'HEAD'],
866 stderr=subprocess2.VOID, error_ok=True).strip()
867 if not branchref:
868 return None
869 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000870 self.branch = ShortBranchName(self.branchref)
871 return self.branch
872
873 def GetBranchRef(self):
874 """Returns the full branch name, e.g. 'refs/heads/master'."""
875 self.GetBranch() # Poke the lazy loader.
876 return self.branchref
877
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000878 @staticmethod
879 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000880 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000881 e.g. 'origin', 'refs/heads/master'
882 """
883 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000884 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
885 error_ok=True).strip()
886 if upstream_branch:
887 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
888 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000889 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
890 error_ok=True).strip()
891 if upstream_branch:
892 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000893 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000894 # Fall back on trying a git-svn upstream branch.
895 if settings.GetIsGitSvn():
896 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000897 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000898 # Else, try to guess the origin remote.
899 remote_branches = RunGit(['branch', '-r']).split()
900 if 'origin/master' in remote_branches:
901 # Fall back on origin/master if it exits.
902 remote = 'origin'
903 upstream_branch = 'refs/heads/master'
904 elif 'origin/trunk' in remote_branches:
905 # Fall back on origin/trunk if it exists. Generally a shared
906 # git-svn clone
907 remote = 'origin'
908 upstream_branch = 'refs/heads/trunk'
909 else:
910 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000911Either pass complete "git diff"-style arguments, like
912 git cl upload origin/master
913or verify this branch is set up to track another (via the --track argument to
914"git checkout -b ...").""")
915
916 return remote, upstream_branch
917
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000918 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000919 upstream_branch = self.GetUpstreamBranch()
920 if not BranchExists(upstream_branch):
921 DieWithError('The upstream for the current branch (%s) does not exist '
922 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +0000923 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000924 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000925
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000926 def GetUpstreamBranch(self):
927 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000928 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000929 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000930 upstream_branch = upstream_branch.replace('refs/heads/',
931 'refs/remotes/%s/' % remote)
932 upstream_branch = upstream_branch.replace('refs/branch-heads/',
933 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000934 self.upstream_branch = upstream_branch
935 return self.upstream_branch
936
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000937 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000938 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000939 remote, branch = None, self.GetBranch()
940 seen_branches = set()
941 while branch not in seen_branches:
942 seen_branches.add(branch)
943 remote, branch = self.FetchUpstreamTuple(branch)
944 branch = ShortBranchName(branch)
945 if remote != '.' or branch.startswith('refs/remotes'):
946 break
947 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000948 remotes = RunGit(['remote'], error_ok=True).split()
949 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000950 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000951 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000952 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000953 logging.warning('Could not determine which remote this change is '
954 'associated with, so defaulting to "%s". This may '
955 'not be what you want. You may prevent this message '
956 'by running "git svn info" as documented here: %s',
957 self._remote,
958 GIT_INSTRUCTIONS_URL)
959 else:
960 logging.warn('Could not determine which remote this change is '
961 'associated with. You may prevent this message by '
962 'running "git svn info" as documented here: %s',
963 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000964 branch = 'HEAD'
965 if branch.startswith('refs/remotes'):
966 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000967 elif branch.startswith('refs/branch-heads/'):
968 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000969 else:
970 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000971 return self._remote
972
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000973 def GitSanityChecks(self, upstream_git_obj):
974 """Checks git repo status and ensures diff is from local commits."""
975
sbc@chromium.org79706062015-01-14 21:18:12 +0000976 if upstream_git_obj is None:
977 if self.GetBranch() is None:
978 print >> sys.stderr, (
dpranke@chromium.orgee87f582015-07-31 18:46:25 +0000979 'ERROR: unable to determine current branch (detached HEAD?)')
sbc@chromium.org79706062015-01-14 21:18:12 +0000980 else:
981 print >> sys.stderr, (
982 'ERROR: no upstream branch')
983 return False
984
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000985 # Verify the commit we're diffing against is in our current branch.
986 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
987 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
988 if upstream_sha != common_ancestor:
989 print >> sys.stderr, (
990 'ERROR: %s is not in the current branch. You may need to rebase '
991 'your tracking branch' % upstream_sha)
992 return False
993
994 # List the commits inside the diff, and verify they are all local.
995 commits_in_diff = RunGit(
996 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
997 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
998 remote_branch = remote_branch.strip()
999 if code != 0:
1000 _, remote_branch = self.GetRemoteBranch()
1001
1002 commits_in_remote = RunGit(
1003 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1004
1005 common_commits = set(commits_in_diff) & set(commits_in_remote)
1006 if common_commits:
1007 print >> sys.stderr, (
1008 'ERROR: Your diff contains %d commits already in %s.\n'
1009 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1010 'the diff. If you are using a custom git flow, you can override'
1011 ' the reference used for this check with "git config '
1012 'gitcl.remotebranch <git-ref>".' % (
1013 len(common_commits), remote_branch, upstream_git_obj))
1014 return False
1015 return True
1016
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001017 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001018 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001019
1020 Returns None if it is not set.
1021 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001022 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
1023 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001024
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001025 def GetGitSvnRemoteUrl(self):
1026 """Return the configured git-svn remote URL parsed from git svn info.
1027
1028 Returns None if it is not set.
1029 """
1030 # URL is dependent on the current directory.
1031 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1032 if data:
1033 keys = dict(line.split(': ', 1) for line in data.splitlines()
1034 if ': ' in line)
1035 return keys.get('URL', None)
1036 return None
1037
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001038 def GetRemoteUrl(self):
1039 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1040
1041 Returns None if there is no remote.
1042 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001043 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001044 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1045
1046 # If URL is pointing to a local directory, it is probably a git cache.
1047 if os.path.isdir(url):
1048 url = RunGit(['config', 'remote.%s.url' % remote],
1049 error_ok=True,
1050 cwd=url).strip()
1051 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001052
1053 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001054 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001055 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001056 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001057 self.issue = int(issue) or None if issue else None
1058 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001059 return self.issue
1060
1061 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +00001062 if not self.rietveld_server:
1063 # If we're on a branch then get the server potentially associated
1064 # with that branch.
1065 if self.GetIssue():
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001066 rietveld_server_config = self._RietveldServer()
1067 if rietveld_server_config:
1068 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1069 ['config', rietveld_server_config], error_ok=True).strip())
evan@chromium.org0af9b702012-02-11 00:42:16 +00001070 if not self.rietveld_server:
1071 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001072 return self.rietveld_server
1073
1074 def GetIssueURL(self):
1075 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001076 if not self.GetIssue():
1077 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001078 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
1079
1080 def GetDescription(self, pretty=False):
1081 if not self.has_description:
1082 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +00001083 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +00001084 try:
1085 self.description = self.RpcServer().get_description(issue).strip()
maruel@chromium.org85616e02014-07-28 15:37:55 +00001086 except urllib2.HTTPError as e:
miket@chromium.org183df1a2012-01-04 19:44:55 +00001087 if e.code == 404:
1088 DieWithError(
1089 ('\nWhile fetching the description for issue %d, received a '
1090 '404 (not found)\n'
1091 'error. It is likely that you deleted this '
1092 'issue on the server. If this is the\n'
1093 'case, please run\n\n'
1094 ' git cl issue 0\n\n'
1095 'to clear the association with the deleted issue. Then run '
1096 'this command again.') % issue)
1097 else:
1098 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +00001099 '\nFailed to fetch issue description. HTTP error %d' % e.code)
maruel@chromium.org85616e02014-07-28 15:37:55 +00001100 except urllib2.URLError as e:
1101 print >> sys.stderr, (
1102 'Warning: Failed to retrieve CL description due to network '
1103 'failure.')
1104 self.description = ''
1105
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001106 self.has_description = True
1107 if pretty:
1108 wrapper = textwrap.TextWrapper()
1109 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1110 return wrapper.fill(self.description)
1111 return self.description
1112
1113 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001114 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001115 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001116 patchset = RunGit(['config', self._PatchsetSetting()],
1117 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001118 self.patchset = int(patchset) or None if patchset else None
1119 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001120 return self.patchset
1121
1122 def SetPatchset(self, patchset):
1123 """Set this branch's patchset. If patchset=0, clears the patchset."""
1124 if patchset:
1125 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001126 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001127 else:
1128 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001129 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001130 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001131
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001132 def GetMostRecentPatchset(self):
1133 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +00001134
1135 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001136 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001137 '/download/issue%s_%s.diff' % (issue, patchset))
1138
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001139 def GetIssueProperties(self):
1140 if self._props is None:
1141 issue = self.GetIssue()
1142 if not issue:
1143 self._props = {}
1144 else:
1145 self._props = self.RpcServer().get_issue_properties(issue, True)
1146 return self._props
1147
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001148 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001149 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001150
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001151 def AddComment(self, message):
1152 return self.RpcServer().add_comment(self.GetIssue(), message)
1153
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001154 def SetIssue(self, issue):
1155 """Set this branch's issue. If issue=0, clears the issue."""
1156 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001157 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001158 RunGit(['config', self._IssueSetting(), str(issue)])
1159 if self.rietveld_server:
1160 RunGit(['config', self._RietveldServer(), self.rietveld_server])
1161 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +00001162 current_issue = self.GetIssue()
1163 if current_issue:
1164 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001165 self.issue = None
1166 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001167
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001168 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001169 if not self.GitSanityChecks(upstream_branch):
1170 DieWithError('\nGit sanity check failure')
1171
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001172 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001173 if not root:
1174 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001175 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001176
1177 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001178 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001179 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001180 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001181 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001182 except subprocess2.CalledProcessError:
1183 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001184 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001185 'This branch probably doesn\'t exist anymore. To reset the\n'
1186 'tracking branch, please run\n'
1187 ' git branch --set-upstream %s trunk\n'
1188 'replacing trunk with origin/master or the relevant branch') %
1189 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001190
maruel@chromium.org52424302012-08-29 15:14:30 +00001191 issue = self.GetIssue()
1192 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001193 if issue:
1194 description = self.GetDescription()
1195 else:
1196 # If the change was never uploaded, use the log messages of all commits
1197 # up to the branch point, as git cl upload will prefill the description
1198 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001199 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1200 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001201
1202 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001203 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001204 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001205 name,
1206 description,
1207 absroot,
1208 files,
1209 issue,
1210 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001211 author,
1212 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001213
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001214 def GetStatus(self):
1215 """Apply a rough heuristic to give a simple summary of an issue's review
1216 or CQ status, assuming adherence to a common workflow.
1217
1218 Returns None if no issue for this branch, or one of the following keywords:
1219 * 'error' - error from review tool (including deleted issues)
1220 * 'unsent' - not sent for review
1221 * 'waiting' - waiting for review
1222 * 'reply' - waiting for owner to reply to review
1223 * 'lgtm' - LGTM from at least one approved reviewer
1224 * 'commit' - in the commit queue
1225 * 'closed' - closed
1226 """
1227 if not self.GetIssue():
1228 return None
1229
1230 try:
1231 props = self.GetIssueProperties()
1232 except urllib2.HTTPError:
1233 return 'error'
1234
1235 if props.get('closed'):
1236 # Issue is closed.
1237 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001238 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001239 # Issue is in the commit queue.
1240 return 'commit'
1241
1242 try:
1243 reviewers = self.GetApprovingReviewers()
1244 except urllib2.HTTPError:
1245 return 'error'
1246
1247 if reviewers:
1248 # Was LGTM'ed.
1249 return 'lgtm'
1250
1251 messages = props.get('messages') or []
1252
1253 if not messages:
1254 # No message was sent.
1255 return 'unsent'
1256 if messages[-1]['sender'] != props.get('owner_email'):
1257 # Non-LGTM reply from non-owner
1258 return 'reply'
1259 return 'waiting'
1260
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001261 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001262 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001263
1264 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001265 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001266 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001267 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001268 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001269 except presubmit_support.PresubmitFailure, e:
1270 DieWithError(
1271 ('%s\nMaybe your depot_tools is out of date?\n'
1272 'If all fails, contact maruel@') % e)
1273
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001274 def UpdateDescription(self, description):
1275 self.description = description
1276 return self.RpcServer().update_description(
1277 self.GetIssue(), self.description)
1278
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001279 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +00001280 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001281 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001282
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001283 def SetFlag(self, flag, value):
1284 """Patchset must match."""
1285 if not self.GetPatchset():
1286 DieWithError('The patchset needs to match. Send another patchset.')
1287 try:
1288 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001289 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001290 except urllib2.HTTPError, e:
1291 if e.code == 404:
1292 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1293 if e.code == 403:
1294 DieWithError(
1295 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1296 'match?') % (self.GetIssue(), self.GetPatchset()))
1297 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001298
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001299 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001300 """Returns an upload.RpcServer() to access this review's rietveld instance.
1301 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001302 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001303 self._rpc_server = rietveld.CachingRietveld(
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001304 self.GetRietveldServer(),
1305 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001306 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001307
1308 def _IssueSetting(self):
1309 """Return the git setting that stores this change's issue."""
1310 return 'branch.%s.rietveldissue' % self.GetBranch()
1311
1312 def _PatchsetSetting(self):
1313 """Return the git setting that stores this change's most recent patchset."""
1314 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1315
1316 def _RietveldServer(self):
1317 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001318 branch = self.GetBranch()
1319 if branch:
1320 return 'branch.%s.rietveldserver' % branch
1321 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001322
1323
1324def GetCodereviewSettingsInteractively():
1325 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001326 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001327 server = settings.GetDefaultServerUrl(error_ok=True)
1328 prompt = 'Rietveld server (host[:port])'
1329 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +00001330 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001331 if not server and not newserver:
1332 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001333 if newserver:
1334 newserver = gclient_utils.UpgradeToHttps(newserver)
1335 if newserver != server:
1336 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001337
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001338 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001339 prompt = caption
1340 if initial:
1341 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +00001342 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001343 if new_val == 'x':
1344 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001345 elif new_val:
1346 if is_url:
1347 new_val = gclient_utils.UpgradeToHttps(new_val)
1348 if new_val != initial:
1349 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001350
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001351 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001352 SetProperty(settings.GetDefaultPrivateFlag(),
1353 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001354 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001355 'tree-status-url', False)
1356 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +00001357 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
rmistry@google.com5626a922015-02-26 14:03:30 +00001358 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
1359 'run-post-upload-hook', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001360
1361 # TODO: configure a default branch to diff against, rather than this
1362 # svn-based hackery.
1363
1364
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001365class ChangeDescription(object):
1366 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001367 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00001368 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001369
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001370 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00001371 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001372
agable@chromium.org42c20792013-09-12 17:34:49 +00001373 @property # www.logilab.org/ticket/89786
1374 def description(self): # pylint: disable=E0202
1375 return '\n'.join(self._description_lines)
1376
1377 def set_description(self, desc):
1378 if isinstance(desc, basestring):
1379 lines = desc.splitlines()
1380 else:
1381 lines = [line.rstrip() for line in desc]
1382 while lines and not lines[0]:
1383 lines.pop(0)
1384 while lines and not lines[-1]:
1385 lines.pop(-1)
1386 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001387
piman@chromium.org336f9122014-09-04 02:16:55 +00001388 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001389 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001390 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001391 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001392 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001393 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001394
agable@chromium.org42c20792013-09-12 17:34:49 +00001395 # Get the set of R= and TBR= lines and remove them from the desciption.
1396 regexp = re.compile(self.R_LINE)
1397 matches = [regexp.match(line) for line in self._description_lines]
1398 new_desc = [l for i, l in enumerate(self._description_lines)
1399 if not matches[i]]
1400 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001401
agable@chromium.org42c20792013-09-12 17:34:49 +00001402 # Construct new unified R= and TBR= lines.
1403 r_names = []
1404 tbr_names = []
1405 for match in matches:
1406 if not match:
1407 continue
1408 people = cleanup_list([match.group(2).strip()])
1409 if match.group(1) == 'TBR':
1410 tbr_names.extend(people)
1411 else:
1412 r_names.extend(people)
1413 for name in r_names:
1414 if name not in reviewers:
1415 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001416 if add_owners_tbr:
1417 owners_db = owners.Database(change.RepositoryRoot(),
1418 fopen=file, os_path=os.path, glob=glob.glob)
1419 all_reviewers = set(tbr_names + reviewers)
1420 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1421 all_reviewers)
1422 tbr_names.extend(owners_db.reviewers_for(missing_files,
1423 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001424 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1425 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1426
1427 # Put the new lines in the description where the old first R= line was.
1428 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1429 if 0 <= line_loc < len(self._description_lines):
1430 if new_tbr_line:
1431 self._description_lines.insert(line_loc, new_tbr_line)
1432 if new_r_line:
1433 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001434 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001435 if new_r_line:
1436 self.append_footer(new_r_line)
1437 if new_tbr_line:
1438 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001439
1440 def prompt(self):
1441 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001442 self.set_description([
1443 '# Enter a description of the change.',
1444 '# This will be displayed on the codereview site.',
1445 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00001446 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00001447 '--------------------',
1448 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001449
agable@chromium.org42c20792013-09-12 17:34:49 +00001450 regexp = re.compile(self.BUG_LINE)
1451 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00001452 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00001453 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001454 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001455 if not content:
1456 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001457 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001458
1459 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001460 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1461 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001462 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001463 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001464
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001465 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001466 if self._description_lines:
1467 # Add an empty line if either the last line or the new line isn't a tag.
1468 last_line = self._description_lines[-1]
1469 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1470 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1471 self._description_lines.append('')
1472 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001473
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001474 def get_reviewers(self):
1475 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001476 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1477 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001478 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001479
1480
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001481def get_approving_reviewers(props):
1482 """Retrieves the reviewers that approved a CL from the issue properties with
1483 messages.
1484
1485 Note that the list may contain reviewers that are not committer, thus are not
1486 considered by the CQ.
1487 """
1488 return sorted(
1489 set(
1490 message['sender']
1491 for message in props['messages']
1492 if message['approval'] and message['sender'] in props['reviewers']
1493 )
1494 )
1495
1496
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001497def FindCodereviewSettingsFile(filename='codereview.settings'):
1498 """Finds the given file starting in the cwd and going up.
1499
1500 Only looks up to the top of the repository unless an
1501 'inherit-review-settings-ok' file exists in the root of the repository.
1502 """
1503 inherit_ok_file = 'inherit-review-settings-ok'
1504 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001505 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001506 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1507 root = '/'
1508 while True:
1509 if filename in os.listdir(cwd):
1510 if os.path.isfile(os.path.join(cwd, filename)):
1511 return open(os.path.join(cwd, filename))
1512 if cwd == root:
1513 break
1514 cwd = os.path.dirname(cwd)
1515
1516
1517def LoadCodereviewSettingsFromFile(fileobj):
1518 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001519 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001520
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001521 def SetProperty(name, setting, unset_error_ok=False):
1522 fullname = 'rietveld.' + name
1523 if setting in keyvals:
1524 RunGit(['config', fullname, keyvals[setting]])
1525 else:
1526 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1527
1528 SetProperty('server', 'CODE_REVIEW_SERVER')
1529 # Only server setting is required. Other settings can be absent.
1530 # In that case, we ignore errors raised during option deletion attempt.
1531 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001532 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001533 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1534 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001535 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001536 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001537 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
1538 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001539 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001540 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001541 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00001542 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
1543 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001544
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001545 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001546 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001547
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00001548 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
1549 RunGit(['config', 'gerrit.squash-uploads',
1550 keyvals['GERRIT_SQUASH_UPLOADS']])
1551
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001552 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1553 #should be of the form
1554 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1555 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1556 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1557 keyvals['ORIGIN_URL_CONFIG']])
1558
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001559
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001560def urlretrieve(source, destination):
1561 """urllib is broken for SSL connections via a proxy therefore we
1562 can't use urllib.urlretrieve()."""
1563 with open(destination, 'w') as f:
1564 f.write(urllib2.urlopen(source).read())
1565
1566
ukai@chromium.org712d6102013-11-27 00:52:58 +00001567def hasSheBang(fname):
1568 """Checks fname is a #! script."""
1569 with open(fname) as f:
1570 return f.read(2).startswith('#!')
1571
1572
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001573def DownloadHooks(force):
1574 """downloads hooks
1575
1576 Args:
1577 force: True to update hooks. False to install hooks if not present.
1578 """
1579 if not settings.GetIsGerrit():
1580 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001581 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001582 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1583 if not os.access(dst, os.X_OK):
1584 if os.path.exists(dst):
1585 if not force:
1586 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001587 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001588 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001589 if not hasSheBang(dst):
1590 DieWithError('Not a script: %s\n'
1591 'You need to download from\n%s\n'
1592 'into .git/hooks/commit-msg and '
1593 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001594 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1595 except Exception:
1596 if os.path.exists(dst):
1597 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001598 DieWithError('\nFailed to download hooks.\n'
1599 'You need to download from\n%s\n'
1600 'into .git/hooks/commit-msg and '
1601 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001602
1603
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001604@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001605def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001606 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001607
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001608 parser.add_option('--activate-update', action='store_true',
1609 help='activate auto-updating [rietveld] section in '
1610 '.git/config')
1611 parser.add_option('--deactivate-update', action='store_true',
1612 help='deactivate auto-updating [rietveld] section in '
1613 '.git/config')
1614 options, args = parser.parse_args(args)
1615
1616 if options.deactivate_update:
1617 RunGit(['config', 'rietveld.autoupdate', 'false'])
1618 return
1619
1620 if options.activate_update:
1621 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1622 return
1623
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001624 if len(args) == 0:
1625 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001626 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001627 return 0
1628
1629 url = args[0]
1630 if not url.endswith('codereview.settings'):
1631 url = os.path.join(url, 'codereview.settings')
1632
1633 # Load code review settings and download hooks (if available).
1634 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001635 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001636 return 0
1637
1638
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001639def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001640 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001641 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1642 branch = ShortBranchName(branchref)
1643 _, args = parser.parse_args(args)
1644 if not args:
1645 print("Current base-url:")
1646 return RunGit(['config', 'branch.%s.base-url' % branch],
1647 error_ok=False).strip()
1648 else:
1649 print("Setting base-url to %s" % args[0])
1650 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1651 error_ok=False).strip()
1652
1653
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001654def color_for_status(status):
1655 """Maps a Changelist status to color, for CMDstatus and other tools."""
1656 return {
1657 'unsent': Fore.RED,
1658 'waiting': Fore.BLUE,
1659 'reply': Fore.YELLOW,
1660 'lgtm': Fore.GREEN,
1661 'commit': Fore.MAGENTA,
1662 'closed': Fore.CYAN,
1663 'error': Fore.WHITE,
1664 }.get(status, Fore.WHITE)
1665
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001666def fetch_cl_status(branch, auth_config=None):
1667 """Fetches information for an issue and returns (branch, issue, status)."""
1668 cl = Changelist(branchref=branch, auth_config=auth_config)
1669 url = cl.GetIssueURL()
1670 status = cl.GetStatus()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001671
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001672 if url and (not status or status == 'error'):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001673 # The issue probably doesn't exist anymore.
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001674 url += ' (broken)'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001675
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001676 return (branch, url, status)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001677
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001678def get_cl_statuses(
1679 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001680 """Returns a blocking iterable of (branch, issue, color) for given branches.
1681
1682 If fine_grained is true, this will fetch CL statuses from the server.
1683 Otherwise, simply indicate if there's a matching url for the given branches.
1684
1685 If max_processes is specified, it is used as the maximum number of processes
1686 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
1687 spawned.
1688 """
1689 # Silence upload.py otherwise it becomes unwieldly.
1690 upload.verbosity = 0
1691
1692 if fine_grained:
1693 # Process one branch synchronously to work through authentication, then
1694 # spawn processes to process all the other branches in parallel.
1695 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001696 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
1697 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001698
1699 branches_to_fetch = branches[1:]
1700 pool = ThreadPool(
1701 min(max_processes, len(branches_to_fetch))
1702 if max_processes is not None
1703 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001704 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001705 yield x
1706 else:
1707 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1708 for b in branches:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001709 cl = Changelist(branchref=b, auth_config=auth_config)
1710 url = cl.GetIssueURL()
1711 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001712
rmistry@google.com2dd99862015-06-22 12:22:18 +00001713
1714def upload_branch_deps(cl, args):
1715 """Uploads CLs of local branches that are dependents of the current branch.
1716
1717 If the local branch dependency tree looks like:
1718 test1 -> test2.1 -> test3.1
1719 -> test3.2
1720 -> test2.2 -> test3.3
1721
1722 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
1723 run on the dependent branches in this order:
1724 test2.1, test3.1, test3.2, test2.2, test3.3
1725
1726 Note: This function does not rebase your local dependent branches. Use it when
1727 you make a change to the parent branch that will not conflict with its
1728 dependent branches, and you would like their dependencies updated in
1729 Rietveld.
1730 """
1731 if git_common.is_dirty_git_tree('upload-branch-deps'):
1732 return 1
1733
1734 root_branch = cl.GetBranch()
1735 if root_branch is None:
1736 DieWithError('Can\'t find dependent branches from detached HEAD state. '
1737 'Get on a branch!')
1738 if not cl.GetIssue() or not cl.GetPatchset():
1739 DieWithError('Current branch does not have an uploaded CL. We cannot set '
1740 'patchset dependencies without an uploaded CL.')
1741
1742 branches = RunGit(['for-each-ref',
1743 '--format=%(refname:short) %(upstream:short)',
1744 'refs/heads'])
1745 if not branches:
1746 print('No local branches found.')
1747 return 0
1748
1749 # Create a dictionary of all local branches to the branches that are dependent
1750 # on it.
1751 tracked_to_dependents = collections.defaultdict(list)
1752 for b in branches.splitlines():
1753 tokens = b.split()
1754 if len(tokens) == 2:
1755 branch_name, tracked = tokens
1756 tracked_to_dependents[tracked].append(branch_name)
1757
1758 print
1759 print 'The dependent local branches of %s are:' % root_branch
1760 dependents = []
1761 def traverse_dependents_preorder(branch, padding=''):
1762 dependents_to_process = tracked_to_dependents.get(branch, [])
1763 padding += ' '
1764 for dependent in dependents_to_process:
1765 print '%s%s' % (padding, dependent)
1766 dependents.append(dependent)
1767 traverse_dependents_preorder(dependent, padding)
1768 traverse_dependents_preorder(root_branch)
1769 print
1770
1771 if not dependents:
1772 print 'There are no dependent local branches for %s' % root_branch
1773 return 0
1774
1775 print ('This command will checkout all dependent branches and run '
1776 '"git cl upload".')
1777 ask_for_data('[Press enter to continue or ctrl-C to quit]')
1778
andybons@chromium.org962f9462016-02-03 20:00:42 +00001779 # Add a default patchset title to all upload calls in Rietveld.
1780 if not settings.GetIsGerrit():
1781 args.extend(['-t', 'Updated patchset dependency'])
1782
rmistry@google.com2dd99862015-06-22 12:22:18 +00001783 # Record all dependents that failed to upload.
1784 failures = {}
1785 # Go through all dependents, checkout the branch and upload.
1786 try:
1787 for dependent_branch in dependents:
1788 print
1789 print '--------------------------------------'
1790 print 'Running "git cl upload" from %s:' % dependent_branch
1791 RunGit(['checkout', '-q', dependent_branch])
1792 print
1793 try:
1794 if CMDupload(OptionParser(), args) != 0:
1795 print 'Upload failed for %s!' % dependent_branch
1796 failures[dependent_branch] = 1
1797 except: # pylint: disable=W0702
1798 failures[dependent_branch] = 1
1799 print
1800 finally:
1801 # Swap back to the original root branch.
1802 RunGit(['checkout', '-q', root_branch])
1803
1804 print
1805 print 'Upload complete for dependent branches!'
1806 for dependent_branch in dependents:
1807 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
1808 print ' %s : %s' % (dependent_branch, upload_status)
1809 print
1810
1811 return 0
1812
1813
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001814def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001815 """Show status of changelists.
1816
1817 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001818 - Red not sent for review or broken
1819 - Blue waiting for review
1820 - Yellow waiting for you to reply to review
1821 - Green LGTM'ed
1822 - Magenta in the commit queue
1823 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001824
1825 Also see 'git cl comments'.
1826 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001827 parser.add_option('--field',
1828 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001829 parser.add_option('-f', '--fast', action='store_true',
1830 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001831 parser.add_option(
1832 '-j', '--maxjobs', action='store', type=int,
1833 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001834
1835 auth.add_auth_options(parser)
1836 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001837 if args:
1838 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001839 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001840
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001841 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001842 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001843 if options.field.startswith('desc'):
1844 print cl.GetDescription()
1845 elif options.field == 'id':
1846 issueid = cl.GetIssue()
1847 if issueid:
1848 print issueid
1849 elif options.field == 'patch':
1850 patchset = cl.GetPatchset()
1851 if patchset:
1852 print patchset
1853 elif options.field == 'url':
1854 url = cl.GetIssueURL()
1855 if url:
1856 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001857 return 0
1858
1859 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1860 if not branches:
1861 print('No local branch found.')
1862 return 0
1863
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001864 changes = (
1865 Changelist(branchref=b, auth_config=auth_config)
1866 for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001867 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001868 alignment = max(5, max(len(b) for b in branches))
1869 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001870 output = get_cl_statuses(branches,
1871 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001872 max_processes=options.maxjobs,
1873 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001874
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001875 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001876 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001877 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001878 while branch not in branch_statuses:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001879 b, i, status = output.next()
1880 branch_statuses[b] = (i, status)
1881 issue_url, status = branch_statuses.pop(branch)
1882 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001883 reset = Fore.RESET
1884 if not sys.stdout.isatty():
1885 color = ''
1886 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001887 status_str = '(%s)' % status if status else ''
1888 print ' %*s : %s%s %s%s' % (
1889 alignment, ShortBranchName(branch), color, issue_url, status_str,
1890 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001891
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001892 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001893 print
1894 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001895 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00001896 if not cl.GetIssue():
1897 print 'No issue assigned.'
1898 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001899 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00001900 if not options.fast:
1901 print 'Issue description:'
1902 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001903 return 0
1904
1905
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001906def colorize_CMDstatus_doc():
1907 """To be called once in main() to add colors to git cl status help."""
1908 colors = [i for i in dir(Fore) if i[0].isupper()]
1909
1910 def colorize_line(line):
1911 for color in colors:
1912 if color in line.upper():
1913 # Extract whitespaces first and the leading '-'.
1914 indent = len(line) - len(line.lstrip(' ')) + 1
1915 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1916 return line
1917
1918 lines = CMDstatus.__doc__.splitlines()
1919 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1920
1921
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001922@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001923def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001924 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001925
1926 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001927 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00001928 parser.add_option('-r', '--reverse', action='store_true',
1929 help='Lookup the branch(es) for the specified issues. If '
1930 'no issues are specified, all branches with mapped '
1931 'issues will be listed.')
1932 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001933
dnj@chromium.org406c4402015-03-03 17:22:28 +00001934 if options.reverse:
1935 branches = RunGit(['for-each-ref', 'refs/heads',
1936 '--format=%(refname:short)']).splitlines()
1937
1938 # Reverse issue lookup.
1939 issue_branch_map = {}
1940 for branch in branches:
1941 cl = Changelist(branchref=branch)
1942 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
1943 if not args:
1944 args = sorted(issue_branch_map.iterkeys())
1945 for issue in args:
1946 if not issue:
1947 continue
1948 print 'Branch for issue number %s: %s' % (
1949 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
1950 else:
1951 cl = Changelist()
1952 if len(args) > 0:
1953 try:
1954 issue = int(args[0])
1955 except ValueError:
1956 DieWithError('Pass a number to set the issue or none to list it.\n'
1957 'Maybe you want to run git cl status?')
1958 cl.SetIssue(issue)
1959 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001960 return 0
1961
1962
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001963def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001964 """Shows or posts review comments for any changelist."""
1965 parser.add_option('-a', '--add-comment', dest='comment',
1966 help='comment to add to an issue')
1967 parser.add_option('-i', dest='issue',
1968 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00001969 parser.add_option('-j', '--json-file',
1970 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001971 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001972 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001973 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001974
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001975 issue = None
1976 if options.issue:
1977 try:
1978 issue = int(options.issue)
1979 except ValueError:
1980 DieWithError('A review issue id is expected to be a number')
1981
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001982 cl = Changelist(issue=issue, auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001983
1984 if options.comment:
1985 cl.AddComment(options.comment)
1986 return 0
1987
1988 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00001989 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00001990 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00001991 summary.append({
1992 'date': message['date'],
1993 'lgtm': False,
1994 'message': message['text'],
1995 'not_lgtm': False,
1996 'sender': message['sender'],
1997 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001998 if message['disapproval']:
1999 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00002000 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002001 elif message['approval']:
2002 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00002003 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002004 elif message['sender'] == data['owner_email']:
2005 color = Fore.MAGENTA
2006 else:
2007 color = Fore.BLUE
2008 print '\n%s%s %s%s' % (
2009 color, message['date'].split('.', 1)[0], message['sender'],
2010 Fore.RESET)
2011 if message['text'].strip():
2012 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00002013 if options.json_file:
2014 with open(options.json_file, 'wb') as f:
2015 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002016 return 0
2017
2018
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002019def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002020 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00002021 parser.add_option('-d', '--display', action='store_true',
2022 help='Display the description instead of opening an editor')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002023 auth.add_auth_options(parser)
2024 options, _ = parser.parse_args(args)
2025 auth_config = auth.extract_auth_config_from_options(options)
2026 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002027 if not cl.GetIssue():
2028 DieWithError('This branch has no associated changelist.')
2029 description = ChangeDescription(cl.GetDescription())
smut@google.com34fb6b12015-07-13 20:03:26 +00002030 if options.display:
2031 print description.description
2032 return 0
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002033 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00002034 if cl.GetDescription() != description.description:
2035 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002036 return 0
2037
2038
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002039def CreateDescriptionFromLog(args):
2040 """Pulls out the commit log to use as a base for the CL description."""
2041 log_args = []
2042 if len(args) == 1 and not args[0].endswith('.'):
2043 log_args = [args[0] + '..']
2044 elif len(args) == 1 and args[0].endswith('...'):
2045 log_args = [args[0][:-1]]
2046 elif len(args) == 2:
2047 log_args = [args[0] + '..' + args[1]]
2048 else:
2049 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00002050 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002051
2052
thestig@chromium.org44202a22014-03-11 19:22:18 +00002053def CMDlint(parser, args):
2054 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002055 parser.add_option('--filter', action='append', metavar='-x,+y',
2056 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002057 auth.add_auth_options(parser)
2058 options, args = parser.parse_args(args)
2059 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002060
2061 # Access to a protected member _XX of a client class
2062 # pylint: disable=W0212
2063 try:
2064 import cpplint
2065 import cpplint_chromium
2066 except ImportError:
2067 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
2068 return 1
2069
2070 # Change the current working directory before calling lint so that it
2071 # shows the correct base.
2072 previous_cwd = os.getcwd()
2073 os.chdir(settings.GetRoot())
2074 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002075 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002076 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
2077 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00002078 if not files:
2079 print "Cannot lint an empty CL"
2080 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00002081
2082 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002083 command = args + files
2084 if options.filter:
2085 command = ['--filter=' + ','.join(options.filter)] + command
2086 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002087
2088 white_regex = re.compile(settings.GetLintRegex())
2089 black_regex = re.compile(settings.GetLintIgnoreRegex())
2090 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
2091 for filename in filenames:
2092 if white_regex.match(filename):
2093 if black_regex.match(filename):
2094 print "Ignoring file %s" % filename
2095 else:
2096 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
2097 extra_check_functions)
2098 else:
2099 print "Skipping file %s" % filename
2100 finally:
2101 os.chdir(previous_cwd)
2102 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
2103 if cpplint._cpplint_state.error_count != 0:
2104 return 1
2105 return 0
2106
2107
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002108def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002109 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002110 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002111 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002112 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00002113 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002114 auth.add_auth_options(parser)
2115 options, args = parser.parse_args(args)
2116 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002117
sbc@chromium.org71437c02015-04-09 19:29:40 +00002118 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00002119 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002120 return 1
2121
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002122 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002123 if args:
2124 base_branch = args[0]
2125 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002126 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002127 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002128
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002129 cl.RunHook(
2130 committing=not options.upload,
2131 may_prompt=False,
2132 verbose=options.verbose,
2133 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00002134 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002135
2136
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002137def AddChangeIdToCommitMessage(options, args):
2138 """Re-commits using the current message, assumes the commit hook is in
2139 place.
2140 """
2141 log_desc = options.message or CreateDescriptionFromLog(args)
2142 git_command = ['commit', '--amend', '-m', log_desc]
2143 RunGit(git_command)
2144 new_log_desc = CreateDescriptionFromLog(args)
2145 if CHANGE_ID in new_log_desc:
2146 print 'git-cl: Added Change-Id to commit message.'
2147 else:
2148 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
2149
2150
tandrii@chromium.org65874e12016-03-04 12:03:02 +00002151def GenerateGerritChangeId(message):
2152 """Returns Ixxxxxx...xxx change id.
2153
2154 Works the same way as
2155 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
2156 but can be called on demand on all platforms.
2157
2158 The basic idea is to generate git hash of a state of the tree, original commit
2159 message, author/committer info and timestamps.
2160 """
2161 lines = []
2162 tree_hash = RunGitSilent(['write-tree'])
2163 lines.append('tree %s' % tree_hash.strip())
2164 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
2165 if code == 0:
2166 lines.append('parent %s' % parent.strip())
2167 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
2168 lines.append('author %s' % author.strip())
2169 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
2170 lines.append('committer %s' % committer.strip())
2171 lines.append('')
2172 # Note: Gerrit's commit-hook actually cleans message of some lines and
2173 # whitespace. This code is not doing this, but it clearly won't decrease
2174 # entropy.
2175 lines.append(message)
2176 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
2177 stdin='\n'.join(lines))
2178 return 'I%s' % change_hash.strip()
2179
2180
piman@chromium.org336f9122014-09-04 02:16:55 +00002181def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002182 """upload the current branch to gerrit."""
2183 # We assume the remote called "origin" is the one we want.
2184 # It is probably not worthwhile to support different workflows.
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002185 gerrit_remote = 'origin'
luqui@chromium.org609f3952015-05-04 22:47:04 +00002186
2187 remote, remote_branch = cl.GetRemoteBranch()
2188 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2189 pending_prefix='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002190
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002191 change_desc = ChangeDescription(
2192 options.message or CreateDescriptionFromLog(args))
2193 if not change_desc.description:
andybons@chromium.org962f9462016-02-03 20:00:42 +00002194 print "\nDescription is empty. Aborting..."
2195 return 1
2196
2197 if options.title:
2198 print "\nPatch titles (-t) are not supported in Gerrit. Aborting..."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002199 return 1
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002200
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002201 if options.squash:
2202 # Try to get the message from a previous upload.
2203 shadow_branch = 'refs/heads/git_cl_uploads/' + cl.GetBranch()
bauerb@chromium.org13502e02016-02-18 10:18:29 +00002204 message = RunGitSilent(['show', '--format=%B', '-s', shadow_branch])
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002205 if not message:
2206 if not options.force:
2207 change_desc.prompt()
2208
2209 if CHANGE_ID not in change_desc.description:
2210 # Run the commit-msg hook without modifying the head commit by writing
2211 # the commit message to a temporary file and running the hook over it,
2212 # then reading the file back in.
2213 commit_msg_hook = os.path.join(settings.GetRoot(), '.git', 'hooks',
2214 'commit-msg')
2215 file_handle, msg_file = tempfile.mkstemp(text=True,
2216 prefix='commit_msg')
tandrii@chromium.orga83663a2016-01-14 16:01:00 +00002217 logging.debug("%s %s", file_handle, msg_file)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002218 try:
2219 try:
tandrii@chromium.orga83663a2016-01-14 16:01:00 +00002220 try:
2221 fileobj = os.fdopen(file_handle, 'w')
2222 except OSError:
2223 # if fdopen fails, file_handle remains open.
2224 # See https://docs.python.org/2/library/os.html#os.fdopen.
2225 os.close(file_handle)
2226 raise
2227 with fileobj:
2228 # This will close the file_handle.
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002229 fileobj.write(change_desc.description)
tandrii@chromium.orga83663a2016-01-14 16:01:00 +00002230 logging.debug("%s %s finish editing", file_handle, msg_file)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002231 finally:
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002232 RunCommand([commit_msg_hook, msg_file])
2233 change_desc.set_description(gclient_utils.FileRead(msg_file))
2234 finally:
2235 os.remove(msg_file)
2236
2237 if not change_desc.description:
2238 print "Description is empty; aborting."
2239 return 1
2240
2241 message = change_desc.description
2242
2243 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2244 if remote is '.':
2245 # If our upstream branch is local, we base our squashed commit on its
2246 # squashed version.
2247 parent = ('refs/heads/git_cl_uploads/' +
2248 scm.GIT.ShortBranchName(upstream_branch))
2249
2250 # Verify that the upstream branch has been uploaded too, otherwise Gerrit
2251 # will create additional CLs when uploading.
2252 if (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2253 RunGitSilent(['rev-parse', parent + ':'])):
2254 print 'Upload upstream branch ' + upstream_branch + ' first.'
2255 return 1
2256 else:
2257 parent = cl.GetCommonAncestorWithUpstream()
2258
2259 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2260 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2261 '-m', message]).strip()
2262 else:
2263 if CHANGE_ID not in change_desc.description:
2264 AddChangeIdToCommitMessage(options, args)
2265 ref_to_push = 'HEAD'
2266 parent = '%s/%s' % (gerrit_remote, branch)
2267
2268 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2269 ref_to_push)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002270 if len(commits) > 1:
2271 print('WARNING: This will upload %d commits. Run the following command '
2272 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002273 print('git log %s..%s' % (parent, ref_to_push))
2274 print('You can also use `git squash-branch` to squash these into a single '
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002275 'commit.')
2276 ask_for_data('About to upload; enter to confirm.')
2277
piman@chromium.org336f9122014-09-04 02:16:55 +00002278 if options.reviewers or options.tbr_owners:
2279 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002280
ukai@chromium.orge8077812012-02-03 03:41:46 +00002281 receive_options = []
2282 cc = cl.GetCCList().split(',')
2283 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002284 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002285 cc = filter(None, cc)
2286 if cc:
2287 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002288 if change_desc.get_reviewers():
2289 receive_options.extend(
2290 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002291
ukai@chromium.orge8077812012-02-03 03:41:46 +00002292 git_command = ['push']
2293 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00002294 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00002295 ' '.join(receive_options))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002296 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
ukai@chromium.orge8077812012-02-03 03:41:46 +00002297 RunGit(git_command)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002298
2299 if options.squash:
2300 head = RunGit(['rev-parse', 'HEAD']).strip()
2301 RunGit(['update-ref', '-m', 'Uploaded ' + head, shadow_branch, ref_to_push])
2302
ukai@chromium.orge8077812012-02-03 03:41:46 +00002303 # TODO(ukai): parse Change-Id: and set issue number?
2304 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002305
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002306
wittman@chromium.org455dc922015-01-26 20:15:50 +00002307def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
2308 """Computes the remote branch ref to use for the CL.
2309
2310 Args:
2311 remote (str): The git remote for the CL.
2312 remote_branch (str): The git remote branch for the CL.
2313 target_branch (str): The target branch specified by the user.
2314 pending_prefix (str): The pending prefix from the settings.
2315 """
2316 if not (remote and remote_branch):
2317 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002318
wittman@chromium.org455dc922015-01-26 20:15:50 +00002319 if target_branch:
2320 # Cannonicalize branch references to the equivalent local full symbolic
2321 # refs, which are then translated into the remote full symbolic refs
2322 # below.
2323 if '/' not in target_branch:
2324 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
2325 else:
2326 prefix_replacements = (
2327 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
2328 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
2329 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
2330 )
2331 match = None
2332 for regex, replacement in prefix_replacements:
2333 match = re.search(regex, target_branch)
2334 if match:
2335 remote_branch = target_branch.replace(match.group(0), replacement)
2336 break
2337 if not match:
2338 # This is a branch path but not one we recognize; use as-is.
2339 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00002340 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
2341 # Handle the refs that need to land in different refs.
2342 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002343
wittman@chromium.org455dc922015-01-26 20:15:50 +00002344 # Create the true path to the remote branch.
2345 # Does the following translation:
2346 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
2347 # * refs/remotes/origin/master -> refs/heads/master
2348 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
2349 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
2350 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
2351 elif remote_branch.startswith('refs/remotes/%s/' % remote):
2352 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
2353 'refs/heads/')
2354 elif remote_branch.startswith('refs/remotes/branch-heads'):
2355 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
2356 # If a pending prefix exists then replace refs/ with it.
2357 if pending_prefix:
2358 remote_branch = remote_branch.replace('refs/', pending_prefix)
2359 return remote_branch
2360
2361
piman@chromium.org336f9122014-09-04 02:16:55 +00002362def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002363 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002364 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2365 upload_args.extend(['--server', cl.GetRietveldServer()])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002366 upload_args.extend(auth.auth_config_to_command_options(cl.auth_config))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002367 if options.emulate_svn_auto_props:
2368 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002369
2370 change_desc = None
2371
pgervais@chromium.org91141372014-01-09 23:27:20 +00002372 if options.email is not None:
2373 upload_args.extend(['--email', options.email])
2374
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002375 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002376 if options.title:
2377 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00002378 if options.message:
2379 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00002380 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002381 print ("This branch is associated with issue %s. "
2382 "Adding patch to that issue." % cl.GetIssue())
2383 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002384 if options.title:
2385 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00002386 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002387 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00002388 if options.reviewers or options.tbr_owners:
2389 change_desc.update_reviewers(options.reviewers,
2390 options.tbr_owners,
2391 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002392 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002393 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002394
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002395 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002396 print "Description is empty; aborting."
2397 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002398
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002399 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002400 if change_desc.get_reviewers():
2401 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00002402 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002403 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00002404 DieWithError("Must specify reviewers to send email.")
2405 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00002406
2407 # We check this before applying rietveld.private assuming that in
2408 # rietveld.cc only addresses which we can send private CLs to are listed
2409 # if rietveld.private is set, and so we should ignore rietveld.cc only when
2410 # --private is specified explicitly on the command line.
2411 if options.private:
2412 logging.warn('rietveld.cc is ignored since private flag is specified. '
2413 'You need to review and add them manually if necessary.')
2414 cc = cl.GetCCListWithoutDefault()
2415 else:
2416 cc = cl.GetCCList()
2417 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00002418 if cc:
2419 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002420
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002421 if options.private or settings.GetDefaultPrivateFlag() == "True":
2422 upload_args.append('--private')
2423
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002424 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00002425 if not options.find_copies:
2426 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002427
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002428 # Include the upstream repo's URL in the change -- this is useful for
2429 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002430 remote_url = cl.GetGitBaseUrlFromConfig()
2431 if not remote_url:
2432 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002433 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002434 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00002435 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
2436 remote_url = (cl.GetRemoteUrl() + '@'
2437 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002438 if remote_url:
2439 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00002440 remote, remote_branch = cl.GetRemoteBranch()
wittman@chromium.org455dc922015-01-26 20:15:50 +00002441 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2442 settings.GetPendingRefPrefix())
2443 if target_ref:
2444 upload_args.extend(['--target_ref', target_ref])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002445
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002446 # Look for dependent patchsets. See crbug.com/480453 for more details.
2447 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2448 upstream_branch = ShortBranchName(upstream_branch)
2449 if remote is '.':
2450 # A local branch is being tracked.
2451 local_branch = ShortBranchName(upstream_branch)
rmistry@google.com78948ed2015-07-08 23:09:57 +00002452 if settings.GetIsSkipDependencyUpload(local_branch):
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002453 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002454 print ('Skipping dependency patchset upload because git config '
2455 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002456 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002457 else:
2458 auth_config = auth.extract_auth_config_from_options(options)
2459 branch_cl = Changelist(branchref=local_branch, auth_config=auth_config)
2460 branch_cl_issue_url = branch_cl.GetIssueURL()
2461 branch_cl_issue = branch_cl.GetIssue()
2462 branch_cl_patchset = branch_cl.GetPatchset()
2463 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2464 upload_args.extend(
2465 ['--depends_on_patchset', '%s:%s' % (
2466 branch_cl_issue, branch_cl_patchset)])
2467 print
2468 print ('The current branch (%s) is tracking a local branch (%s) with '
2469 'an associated CL.') % (cl.GetBranch(), local_branch)
2470 print 'Adding %s/#ps%s as a dependency patchset.' % (
2471 branch_cl_issue_url, branch_cl_patchset)
2472 print
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002473
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002474 project = settings.GetProject()
2475 if project:
2476 upload_args.extend(['--project', project])
2477
rmistry@google.comef966222015-04-07 11:15:01 +00002478 if options.cq_dry_run:
2479 upload_args.extend(['--cq_dry_run'])
2480
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002481 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00002482 upload_args = ['upload'] + upload_args + args
2483 logging.info('upload.RealMain(%s)', upload_args)
2484 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00002485 issue = int(issue)
2486 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00002487 except KeyboardInterrupt:
2488 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002489 except:
2490 # If we got an exception after the user typed a description for their
2491 # change, back up the description before re-raising.
2492 if change_desc:
2493 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2494 print '\nGot exception while uploading -- saving description to %s\n' \
2495 % backup_path
2496 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002497 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002498 backup_file.close()
2499 raise
2500
2501 if not cl.GetIssue():
2502 cl.SetIssue(issue)
2503 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002504
2505 if options.use_commit_queue:
2506 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002507 return 0
2508
2509
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002510def cleanup_list(l):
2511 """Fixes a list so that comma separated items are put as individual items.
2512
2513 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
2514 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
2515 """
2516 items = sum((i.split(',') for i in l), [])
2517 stripped_items = (i.strip() for i in items)
2518 return sorted(filter(None, stripped_items))
2519
2520
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002521@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002522def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00002523 """Uploads the current changelist to codereview.
2524
2525 Can skip dependency patchset uploads for a branch by running:
2526 git config branch.branch_name.skip-deps-uploads True
2527 To unset run:
2528 git config --unset branch.branch_name.skip-deps-uploads
2529 Can also set the above globally by using the --global flag.
2530 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00002531 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2532 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002533 parser.add_option('--bypass-watchlists', action='store_true',
2534 dest='bypass_watchlists',
2535 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002536 parser.add_option('-f', action='store_true', dest='force',
2537 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002538 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00002539 parser.add_option('-t', dest='title',
2540 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002541 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002542 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002543 help='reviewer email addresses')
2544 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002545 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002546 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00002547 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00002548 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002549 parser.add_option('--emulate_svn_auto_props',
2550 '--emulate-svn-auto-props',
2551 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00002552 dest="emulate_svn_auto_props",
2553 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00002554 parser.add_option('-c', '--use-commit-queue', action='store_true',
2555 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002556 parser.add_option('--private', action='store_true',
2557 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00002558 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002559 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00002560 metavar='TARGET',
2561 help='Apply CL to remote ref TARGET. ' +
2562 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002563 parser.add_option('--squash', action='store_true',
2564 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002565 parser.add_option('--no-squash', action='store_true',
2566 help='Don\'t squash multiple commits into one ' +
2567 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002568 parser.add_option('--email', default=None,
2569 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00002570 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
2571 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00002572 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
2573 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00002574 help='Send the patchset to do a CQ dry run right after '
2575 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00002576 parser.add_option('--dependencies', action='store_true',
2577 help='Uploads CLs of all the local branches that depend on '
2578 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002579
rmistry@google.com2dd99862015-06-22 12:22:18 +00002580 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002581 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002582 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002583 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002584 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002585
sbc@chromium.org71437c02015-04-09 19:29:40 +00002586 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002587 return 1
2588
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002589 options.reviewers = cleanup_list(options.reviewers)
2590 options.cc = cleanup_list(options.cc)
2591
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002592 cl = Changelist(auth_config=auth_config)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002593 if args:
2594 # TODO(ukai): is it ok for gerrit case?
2595 base_branch = args[0]
2596 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00002597 if cl.GetBranch() is None:
2598 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
2599
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002600 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002601 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00002602 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00002603
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00002604 # Make sure authenticated to Rietveld before running expensive hooks. It is
2605 # a fast, best efforts check. Rietveld still can reject the authentication
2606 # during the actual upload.
2607 if not settings.GetIsGerrit() and auth_config.use_oauth2:
2608 authenticator = auth.get_authenticator_for_host(
2609 cl.GetRietveldServer(), auth_config)
2610 if not authenticator.has_cached_credentials():
2611 raise auth.LoginRequiredError(cl.GetRietveldServer())
2612
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002613 # Apply watchlists on upload.
2614 change = cl.GetChange(base_branch, None)
2615 watchlist = watchlists.Watchlists(change.RepositoryRoot())
2616 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002617 if not options.bypass_watchlists:
2618 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002619
ukai@chromium.orge8077812012-02-03 03:41:46 +00002620 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00002621 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002622 # Set the reviewer list now so that presubmit checks can access it.
2623 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00002624 change_description.update_reviewers(options.reviewers,
2625 options.tbr_owners,
2626 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002627 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002628 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00002629 may_prompt=not options.force,
2630 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002631 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002632 if not hook_results.should_continue():
2633 return 1
2634 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002635 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002636
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002637 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002638 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002639 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00002640 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002641 print ('The last upload made from this repository was patchset #%d but '
2642 'the most recent patchset on the server is #%d.'
2643 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00002644 print ('Uploading will still work, but if you\'ve uploaded to this issue '
2645 'from another machine or branch the patch you\'re uploading now '
2646 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002647 ask_for_data('About to upload; enter to confirm.')
2648
iannucci@chromium.org79540052012-10-19 23:15:26 +00002649 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002650 if settings.GetIsGerrit():
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002651 if options.squash and options.no_squash:
2652 DieWithError('Can only use one of --squash or --no-squash')
2653
2654 options.squash = ((settings.GetSquashGerritUploads() or options.squash) and
2655 not options.no_squash)
2656
tandrii@chromium.org1e67bb72016-02-11 12:15:49 +00002657 ret = GerritUpload(options, args, cl, change)
2658 else:
2659 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002660 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00002661 git_set_branch_value('last-upload-hash',
2662 RunGit(['rev-parse', 'HEAD']).strip())
rmistry@google.com5626a922015-02-26 14:03:30 +00002663 # Run post upload hooks, if specified.
2664 if settings.GetRunPostUploadHook():
2665 presubmit_support.DoPostUploadExecuter(
2666 change,
2667 cl,
2668 settings.GetRoot(),
2669 options.verbose,
2670 sys.stdout)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002671
rmistry@google.com2dd99862015-06-22 12:22:18 +00002672 # Upload all dependencies if specified.
2673 if options.dependencies:
2674 print
2675 print '--dependencies has been specified.'
2676 print 'All dependent local branches will be re-uploaded.'
2677 print
2678 # Remove the dependencies flag from args so that we do not end up in a
2679 # loop.
2680 orig_args.remove('--dependencies')
2681 upload_branch_deps(cl, orig_args)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002682 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00002683
2684
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002685def IsSubmoduleMergeCommit(ref):
2686 # When submodules are added to the repo, we expect there to be a single
2687 # non-git-svn merge commit at remote HEAD with a signature comment.
2688 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00002689 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002690 return RunGit(cmd) != ''
2691
2692
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002693def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002694 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002695
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002696 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002697 Updates changelog with metadata (e.g. pointer to review).
2698 Pushes/dcommits the code upstream.
2699 Updates review and closes.
2700 """
2701 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2702 help='bypass upload presubmit hook')
2703 parser.add_option('-m', dest='message',
2704 help="override review description")
2705 parser.add_option('-f', action='store_true', dest='force',
2706 help="force yes to questions (don't prompt)")
2707 parser.add_option('-c', dest='contributor',
2708 help="external contributor for patch (appended to " +
2709 "description and used as author for git). Should be " +
2710 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002711 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002712 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002713 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002714 auth_config = auth.extract_auth_config_from_options(options)
2715
2716 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002717
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002718 current = cl.GetBranch()
2719 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2720 if not settings.GetIsGitSvn() and remote == '.':
2721 print
2722 print 'Attempting to push branch %r into another local branch!' % current
2723 print
2724 print 'Either reparent this branch on top of origin/master:'
2725 print ' git reparent-branch --root'
2726 print
2727 print 'OR run `git rebase-update` if you think the parent branch is already'
2728 print 'committed.'
2729 print
2730 print ' Current parent: %r' % upstream_branch
2731 return 1
2732
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002733 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002734 # Default to merging against our best guess of the upstream branch.
2735 args = [cl.GetUpstreamBranch()]
2736
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002737 if options.contributor:
2738 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
2739 print "Please provide contibutor as 'First Last <email@example.com>'"
2740 return 1
2741
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002742 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002743 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002744
sbc@chromium.org71437c02015-04-09 19:29:40 +00002745 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002746 return 1
2747
2748 # This rev-list syntax means "show all commits not in my branch that
2749 # are in base_branch".
2750 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
2751 base_branch]).splitlines()
2752 if upstream_commits:
2753 print ('Base branch "%s" has %d commits '
2754 'not in this branch.' % (base_branch, len(upstream_commits)))
2755 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
2756 return 1
2757
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002758 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002759 svn_head = None
2760 if cmd == 'dcommit' or base_has_submodules:
2761 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
2762 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002763
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002764 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002765 # If the base_head is a submodule merge commit, the first parent of the
2766 # base_head should be a git-svn commit, which is what we're interested in.
2767 base_svn_head = base_branch
2768 if base_has_submodules:
2769 base_svn_head += '^1'
2770
2771 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002772 if extra_commits:
2773 print ('This branch has %d additional commits not upstreamed yet.'
2774 % len(extra_commits.splitlines()))
2775 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
2776 'before attempting to %s.' % (base_branch, cmd))
2777 return 1
2778
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002779 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002780 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002781 author = None
2782 if options.contributor:
2783 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002784 hook_results = cl.RunHook(
2785 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002786 may_prompt=not options.force,
2787 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002788 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002789 if not hook_results.should_continue():
2790 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002791
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002792 # Check the tree status if the tree status URL is set.
2793 status = GetTreeStatus()
2794 if 'closed' == status:
2795 print('The tree is closed. Please wait for it to reopen. Use '
2796 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2797 return 1
2798 elif 'unknown' == status:
2799 print('Unable to determine tree status. Please verify manually and '
2800 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2801 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002802
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002803 change_desc = ChangeDescription(options.message)
2804 if not change_desc.description and cl.GetIssue():
2805 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002806
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002807 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00002808 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002809 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00002810 else:
2811 print 'No description set.'
2812 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
2813 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002814
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002815 # Keep a separate copy for the commit message, because the commit message
2816 # contains the link to the Rietveld issue, while the Rietveld message contains
2817 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002818 # Keep a separate copy for the commit message.
2819 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00002820 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002821
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002822 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00002823 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00002824 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00002825 # after it. Add a period on a new line to circumvent this. Also add a space
2826 # before the period to make sure that Gitiles continues to correctly resolve
2827 # the URL.
2828 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002829 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002830 commit_desc.append_footer('Patch from %s.' % options.contributor)
2831
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00002832 print('Description:')
2833 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002834
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002835 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002836 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00002837 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002838
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002839 # We want to squash all this branch's commits into one commit with the proper
2840 # description. We do this by doing a "reset --soft" to the base branch (which
2841 # keeps the working copy the same), then dcommitting that. If origin/master
2842 # has a submodule merge commit, we'll also need to cherry-pick the squashed
2843 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002844 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002845 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
2846 # Delete the branches if they exist.
2847 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
2848 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
2849 result = RunGitWithCode(showref_cmd)
2850 if result[0] == 0:
2851 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002852
2853 # We might be in a directory that's present in this branch but not in the
2854 # trunk. Move up to the top of the tree so that git commands that expect a
2855 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002856 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002857 if rel_base_path:
2858 os.chdir(rel_base_path)
2859
2860 # Stuff our change into the merge branch.
2861 # We wrap in a try...finally block so if anything goes wrong,
2862 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002863 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002864 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002865 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002866 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002867 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00002868 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002869 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002870 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002871 RunGit(
2872 [
2873 'commit', '--author', options.contributor,
2874 '-m', commit_desc.description,
2875 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002876 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002877 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002878 if base_has_submodules:
2879 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
2880 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
2881 RunGit(['checkout', CHERRY_PICK_BRANCH])
2882 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002883 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002884 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002885 pending_prefix = settings.GetPendingRefPrefix()
2886 if not pending_prefix or branch.startswith(pending_prefix):
2887 # If not using refs/pending/heads/* at all, or target ref is already set
2888 # to pending, then push to the target ref directly.
2889 retcode, output = RunGitWithCode(
2890 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002891 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002892 else:
2893 # Cherry-pick the change on top of pending ref and then push it.
2894 assert branch.startswith('refs/'), branch
2895 assert pending_prefix[-1] == '/', pending_prefix
2896 pending_ref = pending_prefix + branch[len('refs/'):]
2897 retcode, output = PushToGitPending(remote, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002898 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002899 if retcode == 0:
2900 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002901 else:
2902 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002903 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002904 'svn', 'dcommit',
2905 '-C%s' % options.similarity,
2906 '--no-rebase', '--rmdir',
2907 ]
2908 if settings.GetForceHttpsCommitUrl():
2909 # Allow forcing https commit URLs for some projects that don't allow
2910 # committing to http URLs (like Google Code).
2911 remote_url = cl.GetGitSvnRemoteUrl()
2912 if urlparse.urlparse(remote_url).scheme == 'http':
2913 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002914 cmd_args.append('--commit-url=%s' % remote_url)
2915 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002916 if 'Committed r' in output:
2917 revision = re.match(
2918 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2919 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002920 finally:
2921 # And then swap back to the original branch and clean up.
2922 RunGit(['checkout', '-q', cl.GetBranch()])
2923 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002924 if base_has_submodules:
2925 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002926
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002927 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002928 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002929 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002930
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002931 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002932 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002933 try:
2934 revision = WaitForRealCommit(remote, revision, base_branch, branch)
2935 # We set pushed_to_pending to False, since it made it all the way to the
2936 # real ref.
2937 pushed_to_pending = False
2938 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002939 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002940
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002941 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002942 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002943 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002944 if not to_pending:
2945 if viewvc_url and revision:
2946 change_desc.append_footer(
2947 'Committed: %s%s' % (viewvc_url, revision))
2948 elif revision:
2949 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002950 print ('Closing issue '
2951 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002952 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002953 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002954 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002955 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00002956 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00002957 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002958 if options.bypass_hooks:
2959 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2960 else:
2961 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002962 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002963 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002964
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002965 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002966 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
2967 print 'The commit is in the pending queue (%s).' % pending_ref
2968 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00002969 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002970 'footer.' % branch)
2971
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002972 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2973 if os.path.isfile(hook):
2974 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002975
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002976 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002977
2978
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002979def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
2980 print
2981 print 'Waiting for commit to be landed on %s...' % real_ref
2982 print '(If you are impatient, you may Ctrl-C once without harm)'
2983 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
2984 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
2985
2986 loop = 0
2987 while True:
2988 sys.stdout.write('fetching (%d)... \r' % loop)
2989 sys.stdout.flush()
2990 loop += 1
2991
2992 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
2993 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2994 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
2995 for commit in commits.splitlines():
2996 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
2997 print 'Found commit on %s' % real_ref
2998 return commit
2999
3000 current_rev = to_rev
3001
3002
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003003def PushToGitPending(remote, pending_ref, upstream_ref):
3004 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
3005
3006 Returns:
3007 (retcode of last operation, output log of last operation).
3008 """
3009 assert pending_ref.startswith('refs/'), pending_ref
3010 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
3011 cherry = RunGit(['rev-parse', 'HEAD']).strip()
3012 code = 0
3013 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003014 max_attempts = 3
3015 attempts_left = max_attempts
3016 while attempts_left:
3017 if attempts_left != max_attempts:
3018 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
3019 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003020
3021 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003022 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003023 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003024 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003025 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003026 print 'Fetch failed with exit code %d.' % code
3027 if out.strip():
3028 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003029 continue
3030
3031 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003032 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003033 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003034 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003035 if code:
3036 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003037 'Your patch doesn\'t apply cleanly to ref \'%s\', '
3038 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003039 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
3040 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003041 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003042 return code, out
3043
3044 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003045 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003046 code, out = RunGitWithCode(
3047 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
3048 if code == 0:
3049 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003050 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003051 return code, out
3052
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003053 print 'Push failed with exit code %d.' % code
3054 if out.strip():
3055 print out.strip()
3056 if IsFatalPushFailure(out):
3057 print (
3058 'Fatal push error. Make sure your .netrc credentials and git '
3059 'user.email are correct and you have push access to the repo.')
3060 return code, out
3061
3062 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003063 return code, out
3064
3065
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003066def IsFatalPushFailure(push_stdout):
3067 """True if retrying push won't help."""
3068 return '(prohibited by Gerrit)' in push_stdout
3069
3070
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003071@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003072def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003073 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003074 if not settings.GetIsGitSvn():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003075 if get_footer_svn_id():
3076 # If it looks like previous commits were mirrored with git-svn.
3077 message = """This repository appears to be a git-svn mirror, but no
3078upstream SVN master is set. You probably need to run 'git auto-svn' once."""
3079 else:
3080 message = """This doesn't appear to be an SVN repository.
3081If your project has a true, writeable git repository, you probably want to run
3082'git cl land' instead.
3083If your project has a git mirror of an upstream SVN master, you probably need
3084to run 'git svn init'.
3085
3086Using the wrong command might cause your commit to appear to succeed, and the
3087review to be closed, without actually landing upstream. If you choose to
3088proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00003089 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00003090 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003091 return SendUpstream(parser, args, 'dcommit')
3092
3093
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003094@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003095def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003096 """Commits the current changelist via git."""
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003097 if settings.GetIsGitSvn() or get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003098 print('This appears to be an SVN repository.')
3099 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003100 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00003101 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003102 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003103
3104
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003105def ParseIssueNum(arg):
3106 """Parses the issue number from args if present otherwise returns None."""
3107 if re.match(r'\d+', arg):
3108 return arg
3109 if arg.startswith('http'):
3110 return re.sub(r'.*/(\d+)/?', r'\1', arg)
3111 return None
3112
3113
3114@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003115def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00003116 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003117 parser.add_option('-b', dest='newbranch',
3118 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003119 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003120 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003121 parser.add_option('-d', '--directory', action='store', metavar='DIR',
3122 help='Change to the directory DIR immediately, '
3123 'before doing anything else.')
3124 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003125 help='failed patches spew .rej files rather than '
3126 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003127 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
3128 help="don't commit after patch applies")
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003129
3130 group = optparse.OptionGroup(parser,
3131 """Options for continuing work on the current issue uploaded
3132from a different clone (e.g. different machine). Must be used independently from
3133the other options. No issue number should be specified, and the branch must have
3134an issue number associated with it""")
3135 group.add_option('--reapply', action='store_true',
3136 dest='reapply',
3137 help="""Reset the branch and reapply the issue.
3138CAUTION: This will undo any local changes in this branch""")
3139
3140 group.add_option('--pull', action='store_true', dest='pull',
3141 help="Performs a pull before reapplying.")
3142 parser.add_option_group(group)
3143
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003144 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003145 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003146 auth_config = auth.extract_auth_config_from_options(options)
3147
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003148 issue_arg = None
3149 if options.reapply :
3150 if len(args) > 0:
3151 parser.error("--reapply implies no additional arguments.")
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003152
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003153 cl = Changelist()
3154 issue_arg = cl.GetIssue()
3155 upstream = cl.GetUpstreamBranch()
3156 if upstream == None:
3157 parser.error("No upstream branch specified. Cannot reset branch")
3158
3159 RunGit(['reset', '--hard', upstream])
3160 if options.pull:
3161 RunGit(['pull'])
3162 else:
3163 if len(args) != 1:
3164 parser.error("Must specify issue number")
3165
3166 issue_arg = ParseIssueNum(args[0])
3167
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003168 # The patch URL works because ParseIssueNum won't do any substitution
3169 # as the re.sub pattern fails to match and just returns it.
3170 if issue_arg == None:
3171 parser.print_help()
3172 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003173
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003174 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003175 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003176 return 1
3177
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003178 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00003179 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003180
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003181 if options.newbranch:
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003182 if options.reapply:
3183 parser.error("--reapply excludes any option other than --pull")
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003184 if options.force:
3185 RunGit(['branch', '-D', options.newbranch],
3186 stderr=subprocess2.PIPE, error_ok=True)
3187 RunGit(['checkout', '-b', options.newbranch,
3188 Changelist().GetUpstreamBranch()])
3189
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003190 return PatchIssue(issue_arg, options.reject, options.nocommit,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003191 options.directory, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003192
3193
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003194def PatchIssue(issue_arg, reject, nocommit, directory, auth_config):
wychen@chromium.orga872e752015-04-28 23:42:18 +00003195 # PatchIssue should never be called with a dirty tree. It is up to the
3196 # caller to check this, but just in case we assert here since the
3197 # consequences of the caller not checking this could be dire.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003198 assert(not git_common.is_dirty_git_tree('apply'))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003199
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003200 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003201 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00003202 issue = int(issue_arg)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003203 cl = Changelist(issue=issue, auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003204 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00003205 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003206 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00003207 # Assume it's a URL to the patch. Default to https.
3208 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
kjellander@chromium.org44424542015-06-02 18:35:29 +00003209 match = re.match(r'(.*?)/download/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00003210 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003211 DieWithError('Must pass an issue ID or full URL for '
3212 '\'Download raw patch set\'')
kjellander@chromium.org44424542015-06-02 18:35:29 +00003213 issue = int(match.group(2))
3214 cl = Changelist(issue=issue, auth_config=auth_config)
3215 cl.rietveld_server = match.group(1)
3216 patchset = int(match.group(3))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00003217 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003218
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003219 # Switch up to the top-level directory, if necessary, in preparation for
3220 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003221 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003222 if top:
3223 os.chdir(top)
3224
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003225 # Git patches have a/ at the beginning of source paths. We strip that out
3226 # with a sed script rather than the -p flag to patch so we can feed either
3227 # Git or svn-style patches into the same apply command.
3228 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003229 try:
3230 patch_data = subprocess2.check_output(
3231 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
3232 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003233 DieWithError('Git patch mungling failed.')
3234 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003235
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003236 # We use "git apply" to apply the patch instead of "patch" so that we can
3237 # pick up file adds.
3238 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003239 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003240 if directory:
3241 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003242 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003243 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003244 elif IsGitVersionAtLeast('1.7.12'):
3245 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003246 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003247 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003248 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003249 except subprocess2.CalledProcessError:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003250 print 'Failed to apply the patch'
3251 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003252
3253 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003254 if not nocommit:
wychen@chromium.org5b3bebb2015-05-28 21:41:43 +00003255 RunGit(['commit', '-m', (cl.GetDescription() + '\n\n' +
3256 'patch from issue %(i)s at patchset '
carlosk@chromium.org71284d92014-11-14 18:12:50 +00003257 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
3258 % {'i': issue, 'p': patchset})])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003259 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003260 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00003261 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00003262 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003263 else:
3264 print "Patch applied to index."
3265 return 0
3266
3267
3268def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003269 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003270 # Provide a wrapper for git svn rebase to help avoid accidental
3271 # git svn dcommit.
3272 # It's the only command that doesn't use parser at all since we just defer
3273 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003274
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003275 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003276
3277
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003278def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003279 """Fetches the tree status and returns either 'open', 'closed',
3280 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003281 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003282 if url:
3283 status = urllib2.urlopen(url).read().lower()
3284 if status.find('closed') != -1 or status == '0':
3285 return 'closed'
3286 elif status.find('open') != -1 or status == '1':
3287 return 'open'
3288 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003289 return 'unset'
3290
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003291
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003292def GetTreeStatusReason():
3293 """Fetches the tree status from a json url and returns the message
3294 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00003295 url = settings.GetTreeStatusUrl()
3296 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003297 connection = urllib2.urlopen(json_url)
3298 status = json.loads(connection.read())
3299 connection.close()
3300 return status['message']
3301
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003302
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003303def GetBuilderMaster(bot_list):
3304 """For a given builder, fetch the master from AE if available."""
3305 map_url = 'https://builders-map.appspot.com/'
3306 try:
3307 master_map = json.load(urllib2.urlopen(map_url))
3308 except urllib2.URLError as e:
3309 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
3310 (map_url, e))
3311 except ValueError as e:
3312 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
3313 if not master_map:
3314 return None, 'Failed to build master map.'
3315
3316 result_master = ''
3317 for bot in bot_list:
3318 builder = bot.split(':', 1)[0]
3319 master_list = master_map.get(builder, [])
3320 if not master_list:
3321 return None, ('No matching master for builder %s.' % builder)
3322 elif len(master_list) > 1:
3323 return None, ('The builder name %s exists in multiple masters %s.' %
3324 (builder, master_list))
3325 else:
3326 cur_master = master_list[0]
3327 if not result_master:
3328 result_master = cur_master
3329 elif result_master != cur_master:
3330 return None, 'The builders do not belong to the same master.'
3331 return result_master, None
3332
3333
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003334def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003335 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003336 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003337 status = GetTreeStatus()
3338 if 'unset' == status:
3339 print 'You must configure your tree status URL by running "git cl config".'
3340 return 2
3341
3342 print "The tree is %s" % status
3343 print
3344 print GetTreeStatusReason()
3345 if status != 'open':
3346 return 1
3347 return 0
3348
3349
maruel@chromium.org15192402012-09-06 12:38:29 +00003350def CMDtry(parser, args):
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003351 """Triggers a try job through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00003352 group = optparse.OptionGroup(parser, "Try job options")
3353 group.add_option(
3354 "-b", "--bot", action="append",
3355 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
3356 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003357 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00003358 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003359 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00003360 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003361 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00003362 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003363 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003364 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003365 "-r", "--revision",
3366 help="Revision to use for the try job; default: the "
3367 "revision will be determined by the try server; see "
3368 "its waterfall for more info")
3369 group.add_option(
3370 "-c", "--clobber", action="store_true", default=False,
3371 help="Force a clobber before building; e.g. don't do an "
3372 "incremental build")
3373 group.add_option(
3374 "--project",
3375 help="Override which project to use. Projects are defined "
3376 "server-side to define what default bot set to use")
3377 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00003378 "-p", "--property", dest="properties", action="append", default=[],
3379 help="Specify generic properties in the form -p key1=value1 -p "
3380 "key2=value2 etc (buildbucket only). The value will be treated as "
3381 "json if decodable, or as string otherwise.")
3382 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003383 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003384 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003385 "--use-rietveld", action="store_true", default=False,
3386 help="Use Rietveld to trigger try jobs.")
3387 group.add_option(
3388 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3389 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00003390 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003391 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00003392 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003393 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00003394
machenbach@chromium.org45453142015-09-15 08:45:22 +00003395 if options.use_rietveld and options.properties:
3396 parser.error('Properties can only be specified with buildbucket')
3397
3398 # Make sure that all properties are prop=value pairs.
3399 bad_params = [x for x in options.properties if '=' not in x]
3400 if bad_params:
3401 parser.error('Got properties with missing "=": %s' % bad_params)
3402
maruel@chromium.org15192402012-09-06 12:38:29 +00003403 if args:
3404 parser.error('Unknown arguments: %s' % args)
3405
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003406 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00003407 if not cl.GetIssue():
3408 parser.error('Need to upload first')
3409
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003410 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00003411 if props.get('closed'):
3412 parser.error('Cannot send tryjobs for a closed CL')
3413
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003414 if props.get('private'):
3415 parser.error('Cannot use trybots with private issue')
3416
maruel@chromium.org15192402012-09-06 12:38:29 +00003417 if not options.name:
3418 options.name = cl.GetBranch()
3419
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003420 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003421 options.master, err_msg = GetBuilderMaster(options.bot)
3422 if err_msg:
3423 parser.error('Tryserver master cannot be found because: %s\n'
3424 'Please manually specify the tryserver master'
3425 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003426
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003427 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003428 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003429 if not options.bot:
3430 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00003431
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003432 # Get try masters from PRESUBMIT.py files.
3433 masters = presubmit_support.DoGetTryMasters(
3434 change,
3435 change.LocalPaths(),
3436 settings.GetRoot(),
3437 None,
3438 None,
3439 options.verbose,
3440 sys.stdout)
3441 if masters:
3442 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00003443
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003444 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
3445 options.bot = presubmit_support.DoGetTrySlaves(
3446 change,
3447 change.LocalPaths(),
3448 settings.GetRoot(),
3449 None,
3450 None,
3451 options.verbose,
3452 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00003453
3454 if not options.bot:
3455 # Get try masters from cq.cfg if any.
3456 # TODO(tandrii): some (but very few) projects store cq.cfg in different
3457 # location.
3458 cq_cfg = os.path.join(change.RepositoryRoot(),
3459 'infra', 'config', 'cq.cfg')
3460 if os.path.exists(cq_cfg):
3461 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00003462 cq_masters = commit_queue.get_master_builder_map(
3463 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00003464 for master, builders in cq_masters.iteritems():
3465 for builder in builders:
3466 # Skip presubmit builders, because these will fail without LGTM.
3467 if 'presubmit' not in builder.lower():
3468 masters.setdefault(master, {})[builder] = ['defaulttests']
3469 if masters:
3470 return masters
3471
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003472 if not options.bot:
3473 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00003474
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003475 builders_and_tests = {}
3476 # TODO(machenbach): The old style command-line options don't support
3477 # multiple try masters yet.
3478 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
3479 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
3480
3481 for bot in old_style:
3482 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003483 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003484 elif ',' in bot:
3485 parser.error('Specify one bot per --bot flag')
3486 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00003487 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003488
3489 for bot, tests in new_style:
3490 builders_and_tests.setdefault(bot, []).extend(tests)
3491
3492 # Return a master map with one master to be backwards compatible. The
3493 # master name defaults to an empty string, which will cause the master
3494 # not to be set on rietveld (deprecated).
3495 return {options.master: builders_and_tests}
3496
3497 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00003498
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003499 for builders in masters.itervalues():
3500 if any('triggered' in b for b in builders):
3501 print >> sys.stderr, (
3502 'ERROR You are trying to send a job to a triggered bot. This type of'
3503 ' bot requires an\ninitial job from a parent (usually a builder). '
3504 'Instead send your job to the parent.\n'
3505 'Bot list: %s' % builders)
3506 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00003507
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00003508 patchset = cl.GetMostRecentPatchset()
3509 if patchset and patchset != cl.GetPatchset():
3510 print(
3511 '\nWARNING Mismatch between local config and server. Did a previous '
3512 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3513 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003514 if options.luci:
3515 trigger_luci_job(cl, masters, options)
3516 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003517 try:
3518 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
3519 except BuildbucketResponseException as ex:
3520 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00003521 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003522 except Exception as e:
3523 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3524 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
3525 e, stacktrace)
3526 return 1
3527 else:
3528 try:
3529 cl.RpcServer().trigger_distributed_try_jobs(
3530 cl.GetIssue(), patchset, options.name, options.clobber,
3531 options.revision, masters)
3532 except urllib2.HTTPError as e:
3533 if e.code == 404:
3534 print('404 from rietveld; '
3535 'did you mean to use "git try" instead of "git cl try"?')
3536 return 1
3537 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003538
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003539 for (master, builders) in sorted(masters.iteritems()):
3540 if master:
3541 print 'Master: %s' % master
3542 length = max(len(builder) for builder in builders)
3543 for builder in sorted(builders):
3544 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00003545 return 0
3546
3547
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00003548def CMDtry_results(parser, args):
3549 group = optparse.OptionGroup(parser, "Try job results options")
3550 group.add_option(
3551 "-p", "--patchset", type=int, help="patchset number if not current.")
3552 group.add_option(
3553 "--print-master", action='store_true', help="print master name as well")
3554 group.add_option(
3555 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3556 help="Host of buildbucket. The default host is %default.")
3557 parser.add_option_group(group)
3558 auth.add_auth_options(parser)
3559 options, args = parser.parse_args(args)
3560 if args:
3561 parser.error('Unrecognized args: %s' % ' '.join(args))
3562
3563 auth_config = auth.extract_auth_config_from_options(options)
3564 cl = Changelist(auth_config=auth_config)
3565 if not cl.GetIssue():
3566 parser.error('Need to upload first')
3567
3568 if not options.patchset:
3569 options.patchset = cl.GetMostRecentPatchset()
3570 if options.patchset and options.patchset != cl.GetPatchset():
3571 print(
3572 '\nWARNING Mismatch between local config and server. Did a previous '
3573 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3574 'Continuing using\npatchset %s.\n' % options.patchset)
3575 try:
3576 jobs = fetch_try_jobs(auth_config, cl, options)
3577 except BuildbucketResponseException as ex:
3578 print 'Buildbucket error: %s' % ex
3579 return 1
3580 except Exception as e:
3581 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3582 print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
3583 e, stacktrace)
3584 return 1
3585 print_tryjobs(options, jobs)
3586 return 0
3587
3588
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003589@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003590def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003591 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003592 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003593 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003594 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003595
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003596 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003597 if args:
3598 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003599 branch = cl.GetBranch()
3600 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003601 cl = Changelist()
3602 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003603
3604 # Clear configured merge-base, if there is one.
3605 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003606 else:
3607 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003608 return 0
3609
3610
thestig@chromium.org00858c82013-12-02 23:08:03 +00003611def CMDweb(parser, args):
3612 """Opens the current CL in the web browser."""
3613 _, args = parser.parse_args(args)
3614 if args:
3615 parser.error('Unrecognized args: %s' % ' '.join(args))
3616
3617 issue_url = Changelist().GetIssueURL()
3618 if not issue_url:
3619 print >> sys.stderr, 'ERROR No issue to open'
3620 return 1
3621
3622 webbrowser.open(issue_url)
3623 return 0
3624
3625
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003626def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003627 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003628 auth.add_auth_options(parser)
3629 options, args = parser.parse_args(args)
3630 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003631 if args:
3632 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003633 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003634 props = cl.GetIssueProperties()
3635 if props.get('private'):
3636 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003637 cl.SetFlag('commit', '1')
3638 return 0
3639
3640
groby@chromium.org411034a2013-02-26 15:12:01 +00003641def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003642 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003643 auth.add_auth_options(parser)
3644 options, args = parser.parse_args(args)
3645 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00003646 if args:
3647 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003648 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00003649 # Ensure there actually is an issue to close.
3650 cl.GetDescription()
3651 cl.CloseIssue()
3652 return 0
3653
3654
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003655def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003656 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003657 auth.add_auth_options(parser)
3658 options, args = parser.parse_args(args)
3659 auth_config = auth.extract_auth_config_from_options(options)
3660 if args:
3661 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003662
3663 # Uncommitted (staged and unstaged) changes will be destroyed by
3664 # "git reset --hard" if there are merging conflicts in PatchIssue().
3665 # Staged changes would be committed along with the patch from last
3666 # upload, hence counted toward the "last upload" side in the final
3667 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003668 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003669 return 1
3670
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003671 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003672 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003673 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003674 if not issue:
3675 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003676 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003677 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003678
3679 # Create a new branch based on the merge-base
3680 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
3681 try:
3682 # Patch in the latest changes from rietveld.
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003683 rtn = PatchIssue(issue, False, False, None, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003684 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003685 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003686 return rtn
3687
wychen@chromium.org06928532015-02-03 02:11:29 +00003688 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003689 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00003690 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003691 finally:
3692 RunGit(['checkout', '-q', branch])
3693 RunGit(['branch', '-D', TMP_BRANCH])
3694
3695 return 0
3696
3697
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003698def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003699 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003700 parser.add_option(
3701 '--no-color',
3702 action='store_true',
3703 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003704 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003705 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003706 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003707
3708 author = RunGit(['config', 'user.email']).strip() or None
3709
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003710 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003711
3712 if args:
3713 if len(args) > 1:
3714 parser.error('Unknown args')
3715 base_branch = args[0]
3716 else:
3717 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003718 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003719
3720 change = cl.GetChange(base_branch, None)
3721 return owners_finder.OwnersFinder(
3722 [f.LocalPath() for f in
3723 cl.GetChange(base_branch, None).AffectedFiles()],
3724 change.RepositoryRoot(), author,
3725 fopen=file, os_path=os.path, glob=glob.glob,
3726 disable_color=options.no_color).run()
3727
3728
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003729def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003730 """Generates a diff command."""
3731 # Generate diff for the current branch's changes.
3732 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
3733 upstream_commit, '--' ]
3734
3735 if args:
3736 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003737 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003738 diff_cmd.append(arg)
3739 else:
3740 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003741
3742 return diff_cmd
3743
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003744def MatchingFileType(file_name, extensions):
3745 """Returns true if the file name ends with one of the given extensions."""
3746 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003747
enne@chromium.org555cfe42014-01-29 18:21:39 +00003748@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003749def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003750 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00003751 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00003752 GN_EXTS = ['.gn', '.gni']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003753 parser.add_option('--full', action='store_true',
3754 help='Reformat the full content of all touched files')
3755 parser.add_option('--dry-run', action='store_true',
3756 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003757 parser.add_option('--python', action='store_true',
3758 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003759 parser.add_option('--diff', action='store_true',
3760 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003761 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003762
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003763 # git diff generates paths against the root of the repository. Change
3764 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003765 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003766 if rel_base_path:
3767 os.chdir(rel_base_path)
3768
digit@chromium.org29e47272013-05-17 17:01:46 +00003769 # Grab the merge-base commit, i.e. the upstream commit of the current
3770 # branch when it was created or the last time it was rebased. This is
3771 # to cover the case where the user may have called "git fetch origin",
3772 # moving the origin branch to a newer commit, but hasn't rebased yet.
3773 upstream_commit = None
3774 cl = Changelist()
3775 upstream_branch = cl.GetUpstreamBranch()
3776 if upstream_branch:
3777 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
3778 upstream_commit = upstream_commit.strip()
3779
3780 if not upstream_commit:
3781 DieWithError('Could not find base commit for this branch. '
3782 'Are you in detached state?')
3783
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003784 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
3785 diff_output = RunGit(changed_files_cmd)
3786 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00003787 # Filter out files deleted by this CL
3788 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003789
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003790 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
3791 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
3792 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00003793 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00003794
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003795 top_dir = os.path.normpath(
3796 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
3797
3798 # Locate the clang-format binary in the checkout
3799 try:
3800 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
3801 except clang_format.NotFoundError, e:
3802 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00003803
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003804 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
3805 # formatted. This is used to block during the presubmit.
3806 return_value = 0
3807
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003808 if clang_diff_files:
3809 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003810 cmd = [clang_format_tool]
3811 if not opts.dry_run and not opts.diff:
3812 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003813 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003814 if opts.diff:
3815 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003816 else:
3817 env = os.environ.copy()
3818 env['PATH'] = str(os.path.dirname(clang_format_tool))
3819 try:
3820 script = clang_format.FindClangFormatScriptInChromiumTree(
3821 'clang-format-diff.py')
3822 except clang_format.NotFoundError, e:
3823 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003824
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003825 cmd = [sys.executable, script, '-p0']
3826 if not opts.dry_run and not opts.diff:
3827 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003828
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003829 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
3830 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003831
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003832 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
3833 if opts.diff:
3834 sys.stdout.write(stdout)
3835 if opts.dry_run and len(stdout) > 0:
3836 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003837
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003838 # Similar code to above, but using yapf on .py files rather than clang-format
3839 # on C/C++ files
3840 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003841 yapf_tool = gclient_utils.FindExecutable('yapf')
3842 if yapf_tool is None:
3843 DieWithError('yapf not found in PATH')
3844
3845 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003846 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003847 cmd = [yapf_tool]
3848 if not opts.dry_run and not opts.diff:
3849 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003850 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003851 if opts.diff:
3852 sys.stdout.write(stdout)
3853 else:
3854 # TODO(sbc): yapf --lines mode still has some issues.
3855 # https://github.com/google/yapf/issues/154
3856 DieWithError('--python currently only works with --full')
3857
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003858 # Dart's formatter does not have the nice property of only operating on
3859 # modified chunks, so hard code full.
3860 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003861 try:
3862 command = [dart_format.FindDartFmtToolInChromiumTree()]
3863 if not opts.dry_run and not opts.diff:
3864 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003865 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003866
ppi@chromium.org6593d932016-03-03 15:41:15 +00003867 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003868 if opts.dry_run and stdout:
3869 return_value = 2
3870 except dart_format.NotFoundError as e:
erikcorry@chromium.org3e445022015-12-17 09:07:26 +00003871 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
3872 'found in this checkout. Files in other languages are still ' +
3873 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003874
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00003875 # Format GN build files. Always run on full build files for canonical form.
3876 if gn_diff_files:
3877 cmd = ['gn', 'format']
3878 if not opts.dry_run and not opts.diff:
3879 cmd.append('--in-place')
3880 for gn_diff_file in gn_diff_files:
3881 stdout = RunCommand(cmd + [gn_diff_file], cwd=top_dir)
3882 if opts.diff:
3883 sys.stdout.write(stdout)
3884
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003885 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003886
3887
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00003888@subcommand.usage('<codereview url or issue id>')
3889def CMDcheckout(parser, args):
3890 """Checks out a branch associated with a given Rietveld issue."""
3891 _, args = parser.parse_args(args)
3892
3893 if len(args) != 1:
3894 parser.print_help()
3895 return 1
3896
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003897 target_issue = ParseIssueNum(args[0])
3898 if target_issue == None:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00003899 parser.print_help()
3900 return 1
3901
3902 key_and_issues = [x.split() for x in RunGit(
3903 ['config', '--local', '--get-regexp', r'branch\..*\.rietveldissue'])
3904 .splitlines()]
3905 branches = []
3906 for key, issue in key_and_issues:
3907 if issue == target_issue:
3908 branches.append(re.sub(r'branch\.(.*)\.rietveldissue', r'\1', key))
3909
3910 if len(branches) == 0:
3911 print 'No branch found for issue %s.' % target_issue
3912 return 1
3913 if len(branches) == 1:
3914 RunGit(['checkout', branches[0]])
3915 else:
3916 print 'Multiple branches match issue %s:' % target_issue
3917 for i in range(len(branches)):
3918 print '%d: %s' % (i, branches[i])
3919 which = raw_input('Choose by index: ')
3920 try:
3921 RunGit(['checkout', branches[int(which)]])
3922 except (IndexError, ValueError):
3923 print 'Invalid selection, not checking out any branch.'
3924 return 1
3925
3926 return 0
3927
3928
maruel@chromium.org29404b52014-09-08 22:58:00 +00003929def CMDlol(parser, args):
3930 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00003931 print zlib.decompress(base64.b64decode(
3932 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
3933 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
3934 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
3935 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00003936 return 0
3937
3938
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003939class OptionParser(optparse.OptionParser):
3940 """Creates the option parse and add --verbose support."""
3941 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003942 optparse.OptionParser.__init__(
3943 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003944 self.add_option(
3945 '-v', '--verbose', action='count', default=0,
3946 help='Use 2 times for more debugging info')
3947
3948 def parse_args(self, args=None, values=None):
3949 options, args = optparse.OptionParser.parse_args(self, args, values)
3950 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
3951 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
3952 return options, args
3953
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003954
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003955def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00003956 if sys.hexversion < 0x02060000:
3957 print >> sys.stderr, (
3958 '\nYour python version %s is unsupported, please upgrade.\n' %
3959 sys.version.split(' ', 1)[0])
3960 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003961
maruel@chromium.orgddd59412011-11-30 14:20:38 +00003962 # Reload settings.
3963 global settings
3964 settings = Settings()
3965
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003966 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003967 dispatcher = subcommand.CommandDispatcher(__name__)
3968 try:
3969 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003970 except auth.AuthenticationError as e:
3971 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003972 except urllib2.HTTPError, e:
3973 if e.code != 500:
3974 raise
3975 DieWithError(
3976 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
3977 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00003978 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003979
3980
3981if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003982 # These affect sys.stdout so do it outside of main() to simplify mocks in
3983 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00003984 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003985 colorama.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00003986 try:
3987 sys.exit(main(sys.argv[1:]))
3988 except KeyboardInterrupt:
3989 sys.stderr.write('interrupted\n')
3990 sys.exit(1)