blob: bd2eae9c8ba58ac163a1b98f56c8f7672add4139 [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
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000051import git_footers
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'
rmistry@google.comc68112d2015-03-03 12:48:06 +000067REFS_THAT_ALIAS_TO_OTHER_REFS = {
68 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
69 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
70}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000071
thestig@chromium.org44202a22014-03-11 19:22:18 +000072# Valid extensions for files we want to lint.
73DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
74DEFAULT_LINT_IGNORE_REGEX = r"$^"
75
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000076# Shortcut since it quickly becomes redundant.
77Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000078
maruel@chromium.orgddd59412011-11-30 14:20:38 +000079# Initialized in main()
80settings = None
81
82
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000083def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000084 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000085 sys.exit(1)
86
87
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000088def GetNoGitPagerEnv():
89 env = os.environ.copy()
90 # 'cat' is a magical git string that disables pagers on all platforms.
91 env['GIT_PAGER'] = 'cat'
92 return env
93
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000094
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000095def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000096 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000097 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000098 except subprocess2.CalledProcessError as e:
99 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000100 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000101 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000102 'Command "%s" failed.\n%s' % (
103 ' '.join(args), error_message or e.stdout or ''))
104 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000105
106
107def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000108 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000109 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000110
111
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000112def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000113 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000114 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000115 if suppress_stderr:
116 stderr = subprocess2.VOID
117 else:
118 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000119 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000120 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000121 stdout=subprocess2.PIPE,
122 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000123 return code, out[0]
124 except ValueError:
125 # When the subprocess fails, it returns None. That triggers a ValueError
126 # when trying to unpack the return value into (out, code).
127 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000128
129
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000130def RunGitSilent(args):
131 """Returns stdout, suppresses stderr and ingores the return code."""
132 return RunGitWithCode(args, suppress_stderr=True)[1]
133
134
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000135def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000136 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000137 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000138 return (version.startswith(prefix) and
139 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000140
141
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000142def BranchExists(branch):
143 """Return True if specified branch exists."""
144 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
145 suppress_stderr=True)
146 return not code
147
148
maruel@chromium.org90541732011-04-01 17:54:18 +0000149def ask_for_data(prompt):
150 try:
151 return raw_input(prompt)
152 except KeyboardInterrupt:
153 # Hide the exception.
154 sys.exit(1)
155
156
iannucci@chromium.org79540052012-10-19 23:15:26 +0000157def git_set_branch_value(key, value):
158 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000159 if not branch:
160 return
161
162 cmd = ['config']
163 if isinstance(value, int):
164 cmd.append('--int')
165 git_key = 'branch.%s.%s' % (branch, key)
166 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000167
168
169def git_get_branch_default(key, default):
170 branch = Changelist().GetBranch()
171 if branch:
172 git_key = 'branch.%s.%s' % (branch, key)
173 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
174 try:
175 return int(stdout.strip())
176 except ValueError:
177 pass
178 return default
179
180
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000181def add_git_similarity(parser):
182 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000183 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000184 help='Sets the percentage that a pair of files need to match in order to'
185 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000186 parser.add_option(
187 '--find-copies', action='store_true',
188 help='Allows git to look for copies.')
189 parser.add_option(
190 '--no-find-copies', action='store_false', dest='find_copies',
191 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000192
193 old_parser_args = parser.parse_args
194 def Parse(args):
195 options, args = old_parser_args(args)
196
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000197 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000198 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000199 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000200 print('Note: Saving similarity of %d%% in git config.'
201 % options.similarity)
202 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000203
iannucci@chromium.org79540052012-10-19 23:15:26 +0000204 options.similarity = max(0, min(options.similarity, 100))
205
206 if options.find_copies is None:
207 options.find_copies = bool(
208 git_get_branch_default('git-find-copies', True))
209 else:
210 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000211
212 print('Using %d%% similarity for rename/copy detection. '
213 'Override with --similarity.' % options.similarity)
214
215 return options, args
216 parser.parse_args = Parse
217
218
machenbach@chromium.org45453142015-09-15 08:45:22 +0000219def _get_properties_from_options(options):
220 properties = dict(x.split('=', 1) for x in options.properties)
221 for key, val in properties.iteritems():
222 try:
223 properties[key] = json.loads(val)
224 except ValueError:
225 pass # If a value couldn't be evaluated, treat it as a string.
226 return properties
227
228
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000229def _prefix_master(master):
230 """Convert user-specified master name to full master name.
231
232 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
233 name, while the developers always use shortened master name
234 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
235 function does the conversion for buildbucket migration.
236 """
237 prefix = 'master.'
238 if master.startswith(prefix):
239 return master
240 return '%s%s' % (prefix, master)
241
242
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000243def _buildbucket_retry(operation_name, http, *args, **kwargs):
244 """Retries requests to buildbucket service and returns parsed json content."""
245 try_count = 0
246 while True:
247 response, content = http.request(*args, **kwargs)
248 try:
249 content_json = json.loads(content)
250 except ValueError:
251 content_json = None
252
253 # Buildbucket could return an error even if status==200.
254 if content_json and content_json.get('error'):
255 msg = 'Error in response. Reason: %s. Message: %s.' % (
256 content_json['error'].get('reason', ''),
257 content_json['error'].get('message', ''))
258 raise BuildbucketResponseException(msg)
259
260 if response.status == 200:
261 if not content_json:
262 raise BuildbucketResponseException(
263 'Buildbucket returns invalid json content: %s.\n'
264 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
265 content)
266 return content_json
267 if response.status < 500 or try_count >= 2:
268 raise httplib2.HttpLib2Error(content)
269
270 # status >= 500 means transient failures.
271 logging.debug('Transient errors when %s. Will retry.', operation_name)
272 time.sleep(0.5 + 1.5*try_count)
273 try_count += 1
274 assert False, 'unreachable'
275
276
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000277def trigger_luci_job(changelist, masters, options):
278 """Send a job to run on LUCI."""
279 issue_props = changelist.GetIssueProperties()
280 issue = changelist.GetIssue()
281 patchset = changelist.GetMostRecentPatchset()
282 for builders_and_tests in sorted(masters.itervalues()):
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000283 # TODO(hinoka et al): add support for other properties.
284 # Currently, this completely ignores testfilter and other properties.
285 for builder in sorted(builders_and_tests):
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000286 luci_trigger.trigger(
287 builder, 'HEAD', issue, patchset, issue_props['project'])
288
289
machenbach@chromium.org45453142015-09-15 08:45:22 +0000290def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000291 rietveld_url = settings.GetDefaultServerUrl()
292 rietveld_host = urlparse.urlparse(rietveld_url).hostname
293 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
294 http = authenticator.authorize(httplib2.Http())
295 http.force_exception_to_status_code = True
296 issue_props = changelist.GetIssueProperties()
297 issue = changelist.GetIssue()
298 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000299 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000300
301 buildbucket_put_url = (
302 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000303 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000304 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
305 hostname=rietveld_host,
306 issue=issue,
307 patch=patchset)
308
309 batch_req_body = {'builds': []}
310 print_text = []
311 print_text.append('Tried jobs on:')
312 for master, builders_and_tests in sorted(masters.iteritems()):
313 print_text.append('Master: %s' % master)
314 bucket = _prefix_master(master)
315 for builder, tests in sorted(builders_and_tests.iteritems()):
316 print_text.append(' %s: %s' % (builder, tests))
317 parameters = {
318 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000319 'changes': [{
320 'author': {'email': issue_props['owner_email']},
321 'revision': options.revision,
322 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000323 'properties': {
324 'category': category,
325 'issue': issue,
326 'master': master,
327 'patch_project': issue_props['project'],
328 'patch_storage': 'rietveld',
329 'patchset': patchset,
330 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000331 'rietveld': rietveld_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000332 },
333 }
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000334 if tests:
335 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000336 if properties:
337 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000338 if options.clobber:
339 parameters['properties']['clobber'] = True
340 batch_req_body['builds'].append(
341 {
342 'bucket': bucket,
343 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000344 'client_operation_id': str(uuid.uuid4()),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000345 'tags': ['builder:%s' % builder,
346 'buildset:%s' % buildset,
347 'master:%s' % master,
348 'user_agent:git_cl_try']
349 }
350 )
351
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000352 _buildbucket_retry(
353 'triggering tryjobs',
354 http,
355 buildbucket_put_url,
356 'PUT',
357 body=json.dumps(batch_req_body),
358 headers={'Content-Type': 'application/json'}
359 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000360 print_text.append('To see results here, run: git cl try-results')
361 print_text.append('To see results in browser, run: git cl web')
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000362 print '\n'.join(print_text)
kjellander@chromium.org44424542015-06-02 18:35:29 +0000363
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000364
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000365def fetch_try_jobs(auth_config, changelist, options):
366 """Fetches tryjobs from buildbucket.
367
368 Returns a map from build id to build info as json dictionary.
369 """
370 rietveld_url = settings.GetDefaultServerUrl()
371 rietveld_host = urlparse.urlparse(rietveld_url).hostname
372 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
373 if authenticator.has_cached_credentials():
374 http = authenticator.authorize(httplib2.Http())
375 else:
376 print ('Warning: Some results might be missing because %s' %
377 # Get the message on how to login.
378 auth.LoginRequiredError(rietveld_host).message)
379 http = httplib2.Http()
380
381 http.force_exception_to_status_code = True
382
383 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
384 hostname=rietveld_host,
385 issue=changelist.GetIssue(),
386 patch=options.patchset)
387 params = {'tag': 'buildset:%s' % buildset}
388
389 builds = {}
390 while True:
391 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
392 hostname=options.buildbucket_host,
393 params=urllib.urlencode(params))
394 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
395 for build in content.get('builds', []):
396 builds[build['id']] = build
397 if 'next_cursor' in content:
398 params['start_cursor'] = content['next_cursor']
399 else:
400 break
401 return builds
402
403
404def print_tryjobs(options, builds):
405 """Prints nicely result of fetch_try_jobs."""
406 if not builds:
407 print 'No tryjobs scheduled'
408 return
409
410 # Make a copy, because we'll be modifying builds dictionary.
411 builds = builds.copy()
412 builder_names_cache = {}
413
414 def get_builder(b):
415 try:
416 return builder_names_cache[b['id']]
417 except KeyError:
418 try:
419 parameters = json.loads(b['parameters_json'])
420 name = parameters['builder_name']
421 except (ValueError, KeyError) as error:
422 print 'WARNING: failed to get builder name for build %s: %s' % (
423 b['id'], error)
424 name = None
425 builder_names_cache[b['id']] = name
426 return name
427
428 def get_bucket(b):
429 bucket = b['bucket']
430 if bucket.startswith('master.'):
431 return bucket[len('master.'):]
432 return bucket
433
434 if options.print_master:
435 name_fmt = '%%-%ds %%-%ds' % (
436 max(len(str(get_bucket(b))) for b in builds.itervalues()),
437 max(len(str(get_builder(b))) for b in builds.itervalues()))
438 def get_name(b):
439 return name_fmt % (get_bucket(b), get_builder(b))
440 else:
441 name_fmt = '%%-%ds' % (
442 max(len(str(get_builder(b))) for b in builds.itervalues()))
443 def get_name(b):
444 return name_fmt % get_builder(b)
445
446 def sort_key(b):
447 return b['status'], b.get('result'), get_name(b), b.get('url')
448
449 def pop(title, f, color=None, **kwargs):
450 """Pop matching builds from `builds` dict and print them."""
451
452 if not sys.stdout.isatty() or color is None:
453 colorize = str
454 else:
455 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
456
457 result = []
458 for b in builds.values():
459 if all(b.get(k) == v for k, v in kwargs.iteritems()):
460 builds.pop(b['id'])
461 result.append(b)
462 if result:
463 print colorize(title)
464 for b in sorted(result, key=sort_key):
465 print ' ', colorize('\t'.join(map(str, f(b))))
466
467 total = len(builds)
468 pop(status='COMPLETED', result='SUCCESS',
469 title='Successes:', color=Fore.GREEN,
470 f=lambda b: (get_name(b), b.get('url')))
471 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
472 title='Infra Failures:', color=Fore.MAGENTA,
473 f=lambda b: (get_name(b), b.get('url')))
474 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
475 title='Failures:', color=Fore.RED,
476 f=lambda b: (get_name(b), b.get('url')))
477 pop(status='COMPLETED', result='CANCELED',
478 title='Canceled:', color=Fore.MAGENTA,
479 f=lambda b: (get_name(b),))
480 pop(status='COMPLETED', result='FAILURE',
481 failure_reason='INVALID_BUILD_DEFINITION',
482 title='Wrong master/builder name:', color=Fore.MAGENTA,
483 f=lambda b: (get_name(b),))
484 pop(status='COMPLETED', result='FAILURE',
485 title='Other failures:',
486 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
487 pop(status='COMPLETED',
488 title='Other finished:',
489 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
490 pop(status='STARTED',
491 title='Started:', color=Fore.YELLOW,
492 f=lambda b: (get_name(b), b.get('url')))
493 pop(status='SCHEDULED',
494 title='Scheduled:',
495 f=lambda b: (get_name(b), 'id=%s' % b['id']))
496 # The last section is just in case buildbucket API changes OR there is a bug.
497 pop(title='Other:',
498 f=lambda b: (get_name(b), 'id=%s' % b['id']))
499 assert len(builds) == 0
500 print 'Total: %d tryjobs' % total
501
502
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000503def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
504 """Return the corresponding git ref if |base_url| together with |glob_spec|
505 matches the full |url|.
506
507 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
508 """
509 fetch_suburl, as_ref = glob_spec.split(':')
510 if allow_wildcards:
511 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
512 if glob_match:
513 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
514 # "branches/{472,597,648}/src:refs/remotes/svn/*".
515 branch_re = re.escape(base_url)
516 if glob_match.group(1):
517 branch_re += '/' + re.escape(glob_match.group(1))
518 wildcard = glob_match.group(2)
519 if wildcard == '*':
520 branch_re += '([^/]*)'
521 else:
522 # Escape and replace surrounding braces with parentheses and commas
523 # with pipe symbols.
524 wildcard = re.escape(wildcard)
525 wildcard = re.sub('^\\\\{', '(', wildcard)
526 wildcard = re.sub('\\\\,', '|', wildcard)
527 wildcard = re.sub('\\\\}$', ')', wildcard)
528 branch_re += wildcard
529 if glob_match.group(3):
530 branch_re += re.escape(glob_match.group(3))
531 match = re.match(branch_re, url)
532 if match:
533 return re.sub('\*$', match.group(1), as_ref)
534
535 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
536 if fetch_suburl:
537 full_url = base_url + '/' + fetch_suburl
538 else:
539 full_url = base_url
540 if full_url == url:
541 return as_ref
542 return None
543
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000544
iannucci@chromium.org79540052012-10-19 23:15:26 +0000545def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000546 """Prints statistics about the change to the user."""
547 # --no-ext-diff is broken in some versions of Git, so try to work around
548 # this by overriding the environment (but there is still a problem if the
549 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000550 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000551 if 'GIT_EXTERNAL_DIFF' in env:
552 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000553
554 if find_copies:
555 similarity_options = ['--find-copies-harder', '-l100000',
556 '-C%s' % similarity]
557 else:
558 similarity_options = ['-M%s' % similarity]
559
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000560 try:
561 stdout = sys.stdout.fileno()
562 except AttributeError:
563 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000564 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000565 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000566 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000567 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000568
569
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000570class BuildbucketResponseException(Exception):
571 pass
572
573
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000574class Settings(object):
575 def __init__(self):
576 self.default_server = None
577 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000578 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000579 self.is_git_svn = None
580 self.svn_branch = None
581 self.tree_status_url = None
582 self.viewvc_url = None
583 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000584 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000585 self.squash_gerrit_uploads = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000586 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000587 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000588 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000589 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000590
591 def LazyUpdateIfNeeded(self):
592 """Updates the settings from a codereview.settings file, if available."""
593 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000594 # The only value that actually changes the behavior is
595 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000596 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000597 error_ok=True
598 ).strip().lower()
599
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000600 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000601 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000602 LoadCodereviewSettingsFromFile(cr_settings_file)
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000603 # set updated to True to avoid infinite calling loop
tandrii@chromium.org18630d62016-03-04 12:06:02 +0000604 # through DownloadGerritHook
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000605 self.updated = True
tandrii@chromium.org18630d62016-03-04 12:06:02 +0000606 DownloadGerritHook(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000607 self.updated = True
608
609 def GetDefaultServerUrl(self, error_ok=False):
610 if not self.default_server:
611 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000612 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000613 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000614 if error_ok:
615 return self.default_server
616 if not self.default_server:
617 error_message = ('Could not find settings file. You must configure '
618 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000619 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000620 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000621 return self.default_server
622
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000623 @staticmethod
624 def GetRelativeRoot():
625 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000626
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000627 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000628 if self.root is None:
629 self.root = os.path.abspath(self.GetRelativeRoot())
630 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000631
632 def GetIsGitSvn(self):
633 """Return true if this repo looks like it's using git-svn."""
634 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000635 if self.GetPendingRefPrefix():
636 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
637 self.is_git_svn = False
638 else:
639 # If you have any "svn-remote.*" config keys, we think you're using svn.
640 self.is_git_svn = RunGitWithCode(
641 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000642 return self.is_git_svn
643
644 def GetSVNBranch(self):
645 if self.svn_branch is None:
646 if not self.GetIsGitSvn():
647 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
648
649 # Try to figure out which remote branch we're based on.
650 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000651 # 1) iterate through our branch history and find the svn URL.
652 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000653
654 # regexp matching the git-svn line that contains the URL.
655 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
656
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000657 # We don't want to go through all of history, so read a line from the
658 # pipe at a time.
659 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000660 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000661 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
662 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000663 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000664 for line in proc.stdout:
665 match = git_svn_re.match(line)
666 if match:
667 url = match.group(1)
668 proc.stdout.close() # Cut pipe.
669 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000670
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000671 if url:
672 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
673 remotes = RunGit(['config', '--get-regexp',
674 r'^svn-remote\..*\.url']).splitlines()
675 for remote in remotes:
676 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000677 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000678 remote = match.group(1)
679 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000680 rewrite_root = RunGit(
681 ['config', 'svn-remote.%s.rewriteRoot' % remote],
682 error_ok=True).strip()
683 if rewrite_root:
684 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000685 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000686 ['config', 'svn-remote.%s.fetch' % remote],
687 error_ok=True).strip()
688 if fetch_spec:
689 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
690 if self.svn_branch:
691 break
692 branch_spec = RunGit(
693 ['config', 'svn-remote.%s.branches' % remote],
694 error_ok=True).strip()
695 if branch_spec:
696 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
697 if self.svn_branch:
698 break
699 tag_spec = RunGit(
700 ['config', 'svn-remote.%s.tags' % remote],
701 error_ok=True).strip()
702 if tag_spec:
703 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
704 if self.svn_branch:
705 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000706
707 if not self.svn_branch:
708 DieWithError('Can\'t guess svn branch -- try specifying it on the '
709 'command line')
710
711 return self.svn_branch
712
713 def GetTreeStatusUrl(self, error_ok=False):
714 if not self.tree_status_url:
715 error_message = ('You must configure your tree status URL by running '
716 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000717 self.tree_status_url = self._GetRietveldConfig(
718 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000719 return self.tree_status_url
720
721 def GetViewVCUrl(self):
722 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000723 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000724 return self.viewvc_url
725
rmistry@google.com90752582014-01-14 21:04:50 +0000726 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000727 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000728
rmistry@google.com78948ed2015-07-08 23:09:57 +0000729 def GetIsSkipDependencyUpload(self, branch_name):
730 """Returns true if specified branch should skip dep uploads."""
731 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
732 error_ok=True)
733
rmistry@google.com5626a922015-02-26 14:03:30 +0000734 def GetRunPostUploadHook(self):
735 run_post_upload_hook = self._GetRietveldConfig(
736 'run-post-upload-hook', error_ok=True)
737 return run_post_upload_hook == "True"
738
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000739 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000740 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000741
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000742 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000743 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000744
ukai@chromium.orge8077812012-02-03 03:41:46 +0000745 def GetIsGerrit(self):
746 """Return true if this repo is assosiated with gerrit code review system."""
747 if self.is_gerrit is None:
748 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
749 return self.is_gerrit
750
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000751 def GetSquashGerritUploads(self):
752 """Return true if uploads to Gerrit should be squashed by default."""
753 if self.squash_gerrit_uploads is None:
754 self.squash_gerrit_uploads = (
755 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
756 error_ok=True).strip() == 'true')
757 return self.squash_gerrit_uploads
758
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000759 def GetGitEditor(self):
760 """Return the editor specified in the git config, or None if none is."""
761 if self.git_editor is None:
762 self.git_editor = self._GetConfig('core.editor', error_ok=True)
763 return self.git_editor or None
764
thestig@chromium.org44202a22014-03-11 19:22:18 +0000765 def GetLintRegex(self):
766 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
767 DEFAULT_LINT_REGEX)
768
769 def GetLintIgnoreRegex(self):
770 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
771 DEFAULT_LINT_IGNORE_REGEX)
772
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000773 def GetProject(self):
774 if not self.project:
775 self.project = self._GetRietveldConfig('project', error_ok=True)
776 return self.project
777
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000778 def GetForceHttpsCommitUrl(self):
779 if not self.force_https_commit_url:
780 self.force_https_commit_url = self._GetRietveldConfig(
781 'force-https-commit-url', error_ok=True)
782 return self.force_https_commit_url
783
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000784 def GetPendingRefPrefix(self):
785 if not self.pending_ref_prefix:
786 self.pending_ref_prefix = self._GetRietveldConfig(
787 'pending-ref-prefix', error_ok=True)
788 return self.pending_ref_prefix
789
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000790 def _GetRietveldConfig(self, param, **kwargs):
791 return self._GetConfig('rietveld.' + param, **kwargs)
792
rmistry@google.com78948ed2015-07-08 23:09:57 +0000793 def _GetBranchConfig(self, branch_name, param, **kwargs):
794 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
795
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000796 def _GetConfig(self, param, **kwargs):
797 self.LazyUpdateIfNeeded()
798 return RunGit(['config', param], **kwargs).strip()
799
800
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000801def ShortBranchName(branch):
802 """Convert a name like 'refs/heads/foo' to just 'foo'."""
803 return branch.replace('refs/heads/', '')
804
805
806class Changelist(object):
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000807 def __init__(self, branchref=None, issue=None, auth_config=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000808 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000809 global settings
810 if not settings:
811 # Happens when git_cl.py is used as a utility library.
812 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000813 settings.GetDefaultServerUrl()
814 self.branchref = branchref
815 if self.branchref:
816 self.branch = ShortBranchName(self.branchref)
817 else:
818 self.branch = None
819 self.rietveld_server = None
820 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000821 self.lookedup_issue = False
822 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000823 self.has_description = False
824 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000825 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000826 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000827 self.cc = None
828 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000829 self._auth_config = auth_config
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000830 self._props = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000831 self._remote = None
832 self._rpc_server = None
833
834 @property
835 def auth_config(self):
836 return self._auth_config
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000837
838 def GetCCList(self):
839 """Return the users cc'd on this CL.
840
841 Return is a string suitable for passing to gcl with the --cc flag.
842 """
843 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000844 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000845 more_cc = ','.join(self.watchers)
846 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
847 return self.cc
848
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000849 def GetCCListWithoutDefault(self):
850 """Return the users cc'd on this CL excluding default ones."""
851 if self.cc is None:
852 self.cc = ','.join(self.watchers)
853 return self.cc
854
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000855 def SetWatchers(self, watchers):
856 """Set the list of email addresses that should be cc'd based on the changed
857 files in this CL.
858 """
859 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000860
861 def GetBranch(self):
862 """Returns the short branch name, e.g. 'master'."""
863 if not self.branch:
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000864 branchref = RunGit(['symbolic-ref', 'HEAD'],
865 stderr=subprocess2.VOID, error_ok=True).strip()
866 if not branchref:
867 return None
868 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000869 self.branch = ShortBranchName(self.branchref)
870 return self.branch
871
872 def GetBranchRef(self):
873 """Returns the full branch name, e.g. 'refs/heads/master'."""
874 self.GetBranch() # Poke the lazy loader.
875 return self.branchref
876
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000877 @staticmethod
878 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000879 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000880 e.g. 'origin', 'refs/heads/master'
881 """
882 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000883 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
884 error_ok=True).strip()
885 if upstream_branch:
886 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
887 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000888 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
889 error_ok=True).strip()
890 if upstream_branch:
891 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000892 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000893 # Fall back on trying a git-svn upstream branch.
894 if settings.GetIsGitSvn():
895 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000896 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000897 # Else, try to guess the origin remote.
898 remote_branches = RunGit(['branch', '-r']).split()
899 if 'origin/master' in remote_branches:
900 # Fall back on origin/master if it exits.
901 remote = 'origin'
902 upstream_branch = 'refs/heads/master'
903 elif 'origin/trunk' in remote_branches:
904 # Fall back on origin/trunk if it exists. Generally a shared
905 # git-svn clone
906 remote = 'origin'
907 upstream_branch = 'refs/heads/trunk'
908 else:
909 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000910Either pass complete "git diff"-style arguments, like
911 git cl upload origin/master
912or verify this branch is set up to track another (via the --track argument to
913"git checkout -b ...").""")
914
915 return remote, upstream_branch
916
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000917 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000918 upstream_branch = self.GetUpstreamBranch()
919 if not BranchExists(upstream_branch):
920 DieWithError('The upstream for the current branch (%s) does not exist '
921 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +0000922 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000923 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000924
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000925 def GetUpstreamBranch(self):
926 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000927 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000928 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000929 upstream_branch = upstream_branch.replace('refs/heads/',
930 'refs/remotes/%s/' % remote)
931 upstream_branch = upstream_branch.replace('refs/branch-heads/',
932 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000933 self.upstream_branch = upstream_branch
934 return self.upstream_branch
935
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000936 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000937 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000938 remote, branch = None, self.GetBranch()
939 seen_branches = set()
940 while branch not in seen_branches:
941 seen_branches.add(branch)
942 remote, branch = self.FetchUpstreamTuple(branch)
943 branch = ShortBranchName(branch)
944 if remote != '.' or branch.startswith('refs/remotes'):
945 break
946 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000947 remotes = RunGit(['remote'], error_ok=True).split()
948 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000949 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000950 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000951 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000952 logging.warning('Could not determine which remote this change is '
953 'associated with, so defaulting to "%s". This may '
954 'not be what you want. You may prevent this message '
955 'by running "git svn info" as documented here: %s',
956 self._remote,
957 GIT_INSTRUCTIONS_URL)
958 else:
959 logging.warn('Could not determine which remote this change is '
960 'associated with. You may prevent this message by '
961 'running "git svn info" as documented here: %s',
962 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000963 branch = 'HEAD'
964 if branch.startswith('refs/remotes'):
965 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000966 elif branch.startswith('refs/branch-heads/'):
967 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000968 else:
969 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000970 return self._remote
971
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000972 def GitSanityChecks(self, upstream_git_obj):
973 """Checks git repo status and ensures diff is from local commits."""
974
sbc@chromium.org79706062015-01-14 21:18:12 +0000975 if upstream_git_obj is None:
976 if self.GetBranch() is None:
977 print >> sys.stderr, (
dpranke@chromium.orgee87f582015-07-31 18:46:25 +0000978 'ERROR: unable to determine current branch (detached HEAD?)')
sbc@chromium.org79706062015-01-14 21:18:12 +0000979 else:
980 print >> sys.stderr, (
981 'ERROR: no upstream branch')
982 return False
983
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000984 # Verify the commit we're diffing against is in our current branch.
985 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
986 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
987 if upstream_sha != common_ancestor:
988 print >> sys.stderr, (
989 'ERROR: %s is not in the current branch. You may need to rebase '
990 'your tracking branch' % upstream_sha)
991 return False
992
993 # List the commits inside the diff, and verify they are all local.
994 commits_in_diff = RunGit(
995 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
996 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
997 remote_branch = remote_branch.strip()
998 if code != 0:
999 _, remote_branch = self.GetRemoteBranch()
1000
1001 commits_in_remote = RunGit(
1002 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1003
1004 common_commits = set(commits_in_diff) & set(commits_in_remote)
1005 if common_commits:
1006 print >> sys.stderr, (
1007 'ERROR: Your diff contains %d commits already in %s.\n'
1008 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1009 'the diff. If you are using a custom git flow, you can override'
1010 ' the reference used for this check with "git config '
1011 'gitcl.remotebranch <git-ref>".' % (
1012 len(common_commits), remote_branch, upstream_git_obj))
1013 return False
1014 return True
1015
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001016 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001017 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001018
1019 Returns None if it is not set.
1020 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001021 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
1022 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001023
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001024 def GetGitSvnRemoteUrl(self):
1025 """Return the configured git-svn remote URL parsed from git svn info.
1026
1027 Returns None if it is not set.
1028 """
1029 # URL is dependent on the current directory.
1030 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1031 if data:
1032 keys = dict(line.split(': ', 1) for line in data.splitlines()
1033 if ': ' in line)
1034 return keys.get('URL', None)
1035 return None
1036
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001037 def GetRemoteUrl(self):
1038 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1039
1040 Returns None if there is no remote.
1041 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001042 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001043 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1044
1045 # If URL is pointing to a local directory, it is probably a git cache.
1046 if os.path.isdir(url):
1047 url = RunGit(['config', 'remote.%s.url' % remote],
1048 error_ok=True,
1049 cwd=url).strip()
1050 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001051
1052 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001053 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001054 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001055 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001056 self.issue = int(issue) or None if issue else None
1057 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001058 return self.issue
1059
1060 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +00001061 if not self.rietveld_server:
1062 # If we're on a branch then get the server potentially associated
1063 # with that branch.
1064 if self.GetIssue():
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001065 rietveld_server_config = self._RietveldServer()
1066 if rietveld_server_config:
1067 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1068 ['config', rietveld_server_config], error_ok=True).strip())
evan@chromium.org0af9b702012-02-11 00:42:16 +00001069 if not self.rietveld_server:
1070 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001071 return self.rietveld_server
1072
1073 def GetIssueURL(self):
1074 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001075 if not self.GetIssue():
1076 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001077 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
1078
1079 def GetDescription(self, pretty=False):
1080 if not self.has_description:
1081 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +00001082 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +00001083 try:
1084 self.description = self.RpcServer().get_description(issue).strip()
maruel@chromium.org85616e02014-07-28 15:37:55 +00001085 except urllib2.HTTPError as e:
miket@chromium.org183df1a2012-01-04 19:44:55 +00001086 if e.code == 404:
1087 DieWithError(
1088 ('\nWhile fetching the description for issue %d, received a '
1089 '404 (not found)\n'
1090 'error. It is likely that you deleted this '
1091 'issue on the server. If this is the\n'
1092 'case, please run\n\n'
1093 ' git cl issue 0\n\n'
1094 'to clear the association with the deleted issue. Then run '
1095 'this command again.') % issue)
1096 else:
1097 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +00001098 '\nFailed to fetch issue description. HTTP error %d' % e.code)
maruel@chromium.org85616e02014-07-28 15:37:55 +00001099 except urllib2.URLError as e:
1100 print >> sys.stderr, (
1101 'Warning: Failed to retrieve CL description due to network '
1102 'failure.')
1103 self.description = ''
1104
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001105 self.has_description = True
1106 if pretty:
1107 wrapper = textwrap.TextWrapper()
1108 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1109 return wrapper.fill(self.description)
1110 return self.description
1111
1112 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001113 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001114 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001115 patchset = RunGit(['config', self._PatchsetSetting()],
1116 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001117 self.patchset = int(patchset) or None if patchset else None
1118 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001119 return self.patchset
1120
1121 def SetPatchset(self, patchset):
1122 """Set this branch's patchset. If patchset=0, clears the patchset."""
1123 if patchset:
1124 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001125 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001126 else:
1127 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001128 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001129 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001130
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001131 def GetMostRecentPatchset(self):
1132 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +00001133
1134 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001135 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001136 '/download/issue%s_%s.diff' % (issue, patchset))
1137
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001138 def GetIssueProperties(self):
1139 if self._props is None:
1140 issue = self.GetIssue()
1141 if not issue:
1142 self._props = {}
1143 else:
1144 self._props = self.RpcServer().get_issue_properties(issue, True)
1145 return self._props
1146
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001147 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001148 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001149
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001150 def AddComment(self, message):
1151 return self.RpcServer().add_comment(self.GetIssue(), message)
1152
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001153 def SetIssue(self, issue):
1154 """Set this branch's issue. If issue=0, clears the issue."""
1155 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001156 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001157 RunGit(['config', self._IssueSetting(), str(issue)])
1158 if self.rietveld_server:
1159 RunGit(['config', self._RietveldServer(), self.rietveld_server])
1160 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +00001161 current_issue = self.GetIssue()
1162 if current_issue:
1163 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001164 self.issue = None
1165 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001166
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001167 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001168 if not self.GitSanityChecks(upstream_branch):
1169 DieWithError('\nGit sanity check failure')
1170
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001171 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001172 if not root:
1173 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001174 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001175
1176 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001177 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001178 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001179 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001180 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001181 except subprocess2.CalledProcessError:
1182 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001183 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001184 'This branch probably doesn\'t exist anymore. To reset the\n'
1185 'tracking branch, please run\n'
1186 ' git branch --set-upstream %s trunk\n'
1187 'replacing trunk with origin/master or the relevant branch') %
1188 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001189
maruel@chromium.org52424302012-08-29 15:14:30 +00001190 issue = self.GetIssue()
1191 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001192 if issue:
1193 description = self.GetDescription()
1194 else:
1195 # If the change was never uploaded, use the log messages of all commits
1196 # up to the branch point, as git cl upload will prefill the description
1197 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001198 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1199 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001200
1201 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001202 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001203 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001204 name,
1205 description,
1206 absroot,
1207 files,
1208 issue,
1209 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001210 author,
1211 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001212
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001213 def GetStatus(self):
1214 """Apply a rough heuristic to give a simple summary of an issue's review
1215 or CQ status, assuming adherence to a common workflow.
1216
1217 Returns None if no issue for this branch, or one of the following keywords:
1218 * 'error' - error from review tool (including deleted issues)
1219 * 'unsent' - not sent for review
1220 * 'waiting' - waiting for review
1221 * 'reply' - waiting for owner to reply to review
1222 * 'lgtm' - LGTM from at least one approved reviewer
1223 * 'commit' - in the commit queue
1224 * 'closed' - closed
1225 """
1226 if not self.GetIssue():
1227 return None
1228
1229 try:
1230 props = self.GetIssueProperties()
1231 except urllib2.HTTPError:
1232 return 'error'
1233
1234 if props.get('closed'):
1235 # Issue is closed.
1236 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001237 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001238 # Issue is in the commit queue.
1239 return 'commit'
1240
1241 try:
1242 reviewers = self.GetApprovingReviewers()
1243 except urllib2.HTTPError:
1244 return 'error'
1245
1246 if reviewers:
1247 # Was LGTM'ed.
1248 return 'lgtm'
1249
1250 messages = props.get('messages') or []
1251
1252 if not messages:
1253 # No message was sent.
1254 return 'unsent'
1255 if messages[-1]['sender'] != props.get('owner_email'):
1256 # Non-LGTM reply from non-owner
1257 return 'reply'
1258 return 'waiting'
1259
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001260 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001261 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001262
1263 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001264 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001265 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001266 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001267 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001268 except presubmit_support.PresubmitFailure, e:
1269 DieWithError(
1270 ('%s\nMaybe your depot_tools is out of date?\n'
1271 'If all fails, contact maruel@') % e)
1272
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001273 def UpdateDescription(self, description):
1274 self.description = description
1275 return self.RpcServer().update_description(
1276 self.GetIssue(), self.description)
1277
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001278 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +00001279 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001280 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001281
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001282 def SetFlag(self, flag, value):
1283 """Patchset must match."""
1284 if not self.GetPatchset():
1285 DieWithError('The patchset needs to match. Send another patchset.')
1286 try:
1287 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001288 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001289 except urllib2.HTTPError, e:
1290 if e.code == 404:
1291 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1292 if e.code == 403:
1293 DieWithError(
1294 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1295 'match?') % (self.GetIssue(), self.GetPatchset()))
1296 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001297
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001298 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001299 """Returns an upload.RpcServer() to access this review's rietveld instance.
1300 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001301 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001302 self._rpc_server = rietveld.CachingRietveld(
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001303 self.GetRietveldServer(),
1304 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001305 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001306
1307 def _IssueSetting(self):
1308 """Return the git setting that stores this change's issue."""
1309 return 'branch.%s.rietveldissue' % self.GetBranch()
1310
1311 def _PatchsetSetting(self):
1312 """Return the git setting that stores this change's most recent patchset."""
1313 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1314
1315 def _RietveldServer(self):
1316 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001317 branch = self.GetBranch()
1318 if branch:
1319 return 'branch.%s.rietveldserver' % branch
1320 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001321
1322
1323def GetCodereviewSettingsInteractively():
1324 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001325 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001326 server = settings.GetDefaultServerUrl(error_ok=True)
1327 prompt = 'Rietveld server (host[:port])'
1328 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +00001329 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001330 if not server and not newserver:
1331 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001332 if newserver:
1333 newserver = gclient_utils.UpgradeToHttps(newserver)
1334 if newserver != server:
1335 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001336
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001337 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001338 prompt = caption
1339 if initial:
1340 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +00001341 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001342 if new_val == 'x':
1343 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001344 elif new_val:
1345 if is_url:
1346 new_val = gclient_utils.UpgradeToHttps(new_val)
1347 if new_val != initial:
1348 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001349
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001350 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001351 SetProperty(settings.GetDefaultPrivateFlag(),
1352 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001353 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001354 'tree-status-url', False)
1355 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +00001356 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
rmistry@google.com5626a922015-02-26 14:03:30 +00001357 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
1358 'run-post-upload-hook', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001359
1360 # TODO: configure a default branch to diff against, rather than this
1361 # svn-based hackery.
1362
1363
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001364class ChangeDescription(object):
1365 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001366 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00001367 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001368
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001369 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00001370 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001371
agable@chromium.org42c20792013-09-12 17:34:49 +00001372 @property # www.logilab.org/ticket/89786
1373 def description(self): # pylint: disable=E0202
1374 return '\n'.join(self._description_lines)
1375
1376 def set_description(self, desc):
1377 if isinstance(desc, basestring):
1378 lines = desc.splitlines()
1379 else:
1380 lines = [line.rstrip() for line in desc]
1381 while lines and not lines[0]:
1382 lines.pop(0)
1383 while lines and not lines[-1]:
1384 lines.pop(-1)
1385 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001386
piman@chromium.org336f9122014-09-04 02:16:55 +00001387 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001388 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001389 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001390 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001391 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001392 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001393
agable@chromium.org42c20792013-09-12 17:34:49 +00001394 # Get the set of R= and TBR= lines and remove them from the desciption.
1395 regexp = re.compile(self.R_LINE)
1396 matches = [regexp.match(line) for line in self._description_lines]
1397 new_desc = [l for i, l in enumerate(self._description_lines)
1398 if not matches[i]]
1399 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001400
agable@chromium.org42c20792013-09-12 17:34:49 +00001401 # Construct new unified R= and TBR= lines.
1402 r_names = []
1403 tbr_names = []
1404 for match in matches:
1405 if not match:
1406 continue
1407 people = cleanup_list([match.group(2).strip()])
1408 if match.group(1) == 'TBR':
1409 tbr_names.extend(people)
1410 else:
1411 r_names.extend(people)
1412 for name in r_names:
1413 if name not in reviewers:
1414 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001415 if add_owners_tbr:
1416 owners_db = owners.Database(change.RepositoryRoot(),
1417 fopen=file, os_path=os.path, glob=glob.glob)
1418 all_reviewers = set(tbr_names + reviewers)
1419 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1420 all_reviewers)
1421 tbr_names.extend(owners_db.reviewers_for(missing_files,
1422 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001423 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1424 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1425
1426 # Put the new lines in the description where the old first R= line was.
1427 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1428 if 0 <= line_loc < len(self._description_lines):
1429 if new_tbr_line:
1430 self._description_lines.insert(line_loc, new_tbr_line)
1431 if new_r_line:
1432 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001433 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001434 if new_r_line:
1435 self.append_footer(new_r_line)
1436 if new_tbr_line:
1437 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001438
1439 def prompt(self):
1440 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001441 self.set_description([
1442 '# Enter a description of the change.',
1443 '# This will be displayed on the codereview site.',
1444 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00001445 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00001446 '--------------------',
1447 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001448
agable@chromium.org42c20792013-09-12 17:34:49 +00001449 regexp = re.compile(self.BUG_LINE)
1450 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00001451 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00001452 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001453 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001454 if not content:
1455 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001456 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001457
1458 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001459 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1460 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001461 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001462 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001463
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001464 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001465 if self._description_lines:
1466 # Add an empty line if either the last line or the new line isn't a tag.
1467 last_line = self._description_lines[-1]
1468 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1469 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1470 self._description_lines.append('')
1471 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001472
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001473 def get_reviewers(self):
1474 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001475 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1476 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001477 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001478
1479
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001480def get_approving_reviewers(props):
1481 """Retrieves the reviewers that approved a CL from the issue properties with
1482 messages.
1483
1484 Note that the list may contain reviewers that are not committer, thus are not
1485 considered by the CQ.
1486 """
1487 return sorted(
1488 set(
1489 message['sender']
1490 for message in props['messages']
1491 if message['approval'] and message['sender'] in props['reviewers']
1492 )
1493 )
1494
1495
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001496def FindCodereviewSettingsFile(filename='codereview.settings'):
1497 """Finds the given file starting in the cwd and going up.
1498
1499 Only looks up to the top of the repository unless an
1500 'inherit-review-settings-ok' file exists in the root of the repository.
1501 """
1502 inherit_ok_file = 'inherit-review-settings-ok'
1503 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001504 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001505 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1506 root = '/'
1507 while True:
1508 if filename in os.listdir(cwd):
1509 if os.path.isfile(os.path.join(cwd, filename)):
1510 return open(os.path.join(cwd, filename))
1511 if cwd == root:
1512 break
1513 cwd = os.path.dirname(cwd)
1514
1515
1516def LoadCodereviewSettingsFromFile(fileobj):
1517 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001518 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001519
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001520 def SetProperty(name, setting, unset_error_ok=False):
1521 fullname = 'rietveld.' + name
1522 if setting in keyvals:
1523 RunGit(['config', fullname, keyvals[setting]])
1524 else:
1525 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1526
1527 SetProperty('server', 'CODE_REVIEW_SERVER')
1528 # Only server setting is required. Other settings can be absent.
1529 # In that case, we ignore errors raised during option deletion attempt.
1530 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001531 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001532 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1533 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001534 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001535 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001536 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
1537 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001538 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001539 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001540 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00001541 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
1542 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001543
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001544 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001545 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001546
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00001547 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
1548 RunGit(['config', 'gerrit.squash-uploads',
1549 keyvals['GERRIT_SQUASH_UPLOADS']])
1550
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001551 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1552 #should be of the form
1553 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1554 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1555 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1556 keyvals['ORIGIN_URL_CONFIG']])
1557
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001558
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001559def urlretrieve(source, destination):
1560 """urllib is broken for SSL connections via a proxy therefore we
1561 can't use urllib.urlretrieve()."""
1562 with open(destination, 'w') as f:
1563 f.write(urllib2.urlopen(source).read())
1564
1565
ukai@chromium.org712d6102013-11-27 00:52:58 +00001566def hasSheBang(fname):
1567 """Checks fname is a #! script."""
1568 with open(fname) as f:
1569 return f.read(2).startswith('#!')
1570
1571
tandrii@chromium.org18630d62016-03-04 12:06:02 +00001572def DownloadGerritHook(force):
1573 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001574
1575 Args:
1576 force: True to update hooks. False to install hooks if not present.
1577 """
1578 if not settings.GetIsGerrit():
1579 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001580 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001581 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1582 if not os.access(dst, os.X_OK):
1583 if os.path.exists(dst):
1584 if not force:
1585 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001586 try:
tandrii@chromium.org18630d62016-03-04 12:06:02 +00001587 print(
1588 'WARNING: installing Gerrit commit-msg hook.\n'
1589 ' This behavior of git cl will soon be disabled.\n'
1590 ' See bug http://crbug.com/579176.')
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001591 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001592 if not hasSheBang(dst):
1593 DieWithError('Not a script: %s\n'
1594 'You need to download from\n%s\n'
1595 'into .git/hooks/commit-msg and '
1596 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001597 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1598 except Exception:
1599 if os.path.exists(dst):
1600 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001601 DieWithError('\nFailed to download hooks.\n'
1602 'You need to download from\n%s\n'
1603 'into .git/hooks/commit-msg and '
1604 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001605
tandrii@chromium.orgc55295c2016-03-04 15:54:59 +00001606# TODO(tandrii): remove this once repos which call this method directly are
1607# upgraded.
1608DownloadHooks = DownloadGerritHook
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001609
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001610@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001611def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001612 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001613
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001614 parser.add_option('--activate-update', action='store_true',
1615 help='activate auto-updating [rietveld] section in '
1616 '.git/config')
1617 parser.add_option('--deactivate-update', action='store_true',
1618 help='deactivate auto-updating [rietveld] section in '
1619 '.git/config')
1620 options, args = parser.parse_args(args)
1621
1622 if options.deactivate_update:
1623 RunGit(['config', 'rietveld.autoupdate', 'false'])
1624 return
1625
1626 if options.activate_update:
1627 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1628 return
1629
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001630 if len(args) == 0:
1631 GetCodereviewSettingsInteractively()
tandrii@chromium.org18630d62016-03-04 12:06:02 +00001632 DownloadGerritHook(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001633 return 0
1634
1635 url = args[0]
1636 if not url.endswith('codereview.settings'):
1637 url = os.path.join(url, 'codereview.settings')
1638
1639 # Load code review settings and download hooks (if available).
1640 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
tandrii@chromium.org18630d62016-03-04 12:06:02 +00001641 DownloadGerritHook(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001642 return 0
1643
1644
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001645def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001646 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001647 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1648 branch = ShortBranchName(branchref)
1649 _, args = parser.parse_args(args)
1650 if not args:
1651 print("Current base-url:")
1652 return RunGit(['config', 'branch.%s.base-url' % branch],
1653 error_ok=False).strip()
1654 else:
1655 print("Setting base-url to %s" % args[0])
1656 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1657 error_ok=False).strip()
1658
1659
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001660def color_for_status(status):
1661 """Maps a Changelist status to color, for CMDstatus and other tools."""
1662 return {
1663 'unsent': Fore.RED,
1664 'waiting': Fore.BLUE,
1665 'reply': Fore.YELLOW,
1666 'lgtm': Fore.GREEN,
1667 'commit': Fore.MAGENTA,
1668 'closed': Fore.CYAN,
1669 'error': Fore.WHITE,
1670 }.get(status, Fore.WHITE)
1671
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001672def fetch_cl_status(branch, auth_config=None):
1673 """Fetches information for an issue and returns (branch, issue, status)."""
1674 cl = Changelist(branchref=branch, auth_config=auth_config)
1675 url = cl.GetIssueURL()
1676 status = cl.GetStatus()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001677
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001678 if url and (not status or status == 'error'):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001679 # The issue probably doesn't exist anymore.
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001680 url += ' (broken)'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001681
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001682 return (branch, url, status)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001683
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001684def get_cl_statuses(
1685 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001686 """Returns a blocking iterable of (branch, issue, color) for given branches.
1687
1688 If fine_grained is true, this will fetch CL statuses from the server.
1689 Otherwise, simply indicate if there's a matching url for the given branches.
1690
1691 If max_processes is specified, it is used as the maximum number of processes
1692 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
1693 spawned.
1694 """
1695 # Silence upload.py otherwise it becomes unwieldly.
1696 upload.verbosity = 0
1697
1698 if fine_grained:
1699 # Process one branch synchronously to work through authentication, then
1700 # spawn processes to process all the other branches in parallel.
1701 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001702 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
1703 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001704
1705 branches_to_fetch = branches[1:]
1706 pool = ThreadPool(
1707 min(max_processes, len(branches_to_fetch))
1708 if max_processes is not None
1709 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001710 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001711 yield x
1712 else:
1713 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1714 for b in branches:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001715 cl = Changelist(branchref=b, auth_config=auth_config)
1716 url = cl.GetIssueURL()
1717 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001718
rmistry@google.com2dd99862015-06-22 12:22:18 +00001719
1720def upload_branch_deps(cl, args):
1721 """Uploads CLs of local branches that are dependents of the current branch.
1722
1723 If the local branch dependency tree looks like:
1724 test1 -> test2.1 -> test3.1
1725 -> test3.2
1726 -> test2.2 -> test3.3
1727
1728 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
1729 run on the dependent branches in this order:
1730 test2.1, test3.1, test3.2, test2.2, test3.3
1731
1732 Note: This function does not rebase your local dependent branches. Use it when
1733 you make a change to the parent branch that will not conflict with its
1734 dependent branches, and you would like their dependencies updated in
1735 Rietveld.
1736 """
1737 if git_common.is_dirty_git_tree('upload-branch-deps'):
1738 return 1
1739
1740 root_branch = cl.GetBranch()
1741 if root_branch is None:
1742 DieWithError('Can\'t find dependent branches from detached HEAD state. '
1743 'Get on a branch!')
1744 if not cl.GetIssue() or not cl.GetPatchset():
1745 DieWithError('Current branch does not have an uploaded CL. We cannot set '
1746 'patchset dependencies without an uploaded CL.')
1747
1748 branches = RunGit(['for-each-ref',
1749 '--format=%(refname:short) %(upstream:short)',
1750 'refs/heads'])
1751 if not branches:
1752 print('No local branches found.')
1753 return 0
1754
1755 # Create a dictionary of all local branches to the branches that are dependent
1756 # on it.
1757 tracked_to_dependents = collections.defaultdict(list)
1758 for b in branches.splitlines():
1759 tokens = b.split()
1760 if len(tokens) == 2:
1761 branch_name, tracked = tokens
1762 tracked_to_dependents[tracked].append(branch_name)
1763
1764 print
1765 print 'The dependent local branches of %s are:' % root_branch
1766 dependents = []
1767 def traverse_dependents_preorder(branch, padding=''):
1768 dependents_to_process = tracked_to_dependents.get(branch, [])
1769 padding += ' '
1770 for dependent in dependents_to_process:
1771 print '%s%s' % (padding, dependent)
1772 dependents.append(dependent)
1773 traverse_dependents_preorder(dependent, padding)
1774 traverse_dependents_preorder(root_branch)
1775 print
1776
1777 if not dependents:
1778 print 'There are no dependent local branches for %s' % root_branch
1779 return 0
1780
1781 print ('This command will checkout all dependent branches and run '
1782 '"git cl upload".')
1783 ask_for_data('[Press enter to continue or ctrl-C to quit]')
1784
andybons@chromium.org962f9462016-02-03 20:00:42 +00001785 # Add a default patchset title to all upload calls in Rietveld.
1786 if not settings.GetIsGerrit():
1787 args.extend(['-t', 'Updated patchset dependency'])
1788
rmistry@google.com2dd99862015-06-22 12:22:18 +00001789 # Record all dependents that failed to upload.
1790 failures = {}
1791 # Go through all dependents, checkout the branch and upload.
1792 try:
1793 for dependent_branch in dependents:
1794 print
1795 print '--------------------------------------'
1796 print 'Running "git cl upload" from %s:' % dependent_branch
1797 RunGit(['checkout', '-q', dependent_branch])
1798 print
1799 try:
1800 if CMDupload(OptionParser(), args) != 0:
1801 print 'Upload failed for %s!' % dependent_branch
1802 failures[dependent_branch] = 1
1803 except: # pylint: disable=W0702
1804 failures[dependent_branch] = 1
1805 print
1806 finally:
1807 # Swap back to the original root branch.
1808 RunGit(['checkout', '-q', root_branch])
1809
1810 print
1811 print 'Upload complete for dependent branches!'
1812 for dependent_branch in dependents:
1813 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
1814 print ' %s : %s' % (dependent_branch, upload_status)
1815 print
1816
1817 return 0
1818
1819
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001820def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001821 """Show status of changelists.
1822
1823 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001824 - Red not sent for review or broken
1825 - Blue waiting for review
1826 - Yellow waiting for you to reply to review
1827 - Green LGTM'ed
1828 - Magenta in the commit queue
1829 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001830
1831 Also see 'git cl comments'.
1832 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001833 parser.add_option('--field',
1834 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001835 parser.add_option('-f', '--fast', action='store_true',
1836 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001837 parser.add_option(
1838 '-j', '--maxjobs', action='store', type=int,
1839 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001840
1841 auth.add_auth_options(parser)
1842 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001843 if args:
1844 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001845 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001846
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001847 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001848 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001849 if options.field.startswith('desc'):
1850 print cl.GetDescription()
1851 elif options.field == 'id':
1852 issueid = cl.GetIssue()
1853 if issueid:
1854 print issueid
1855 elif options.field == 'patch':
1856 patchset = cl.GetPatchset()
1857 if patchset:
1858 print patchset
1859 elif options.field == 'url':
1860 url = cl.GetIssueURL()
1861 if url:
1862 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001863 return 0
1864
1865 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1866 if not branches:
1867 print('No local branch found.')
1868 return 0
1869
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001870 changes = (
1871 Changelist(branchref=b, auth_config=auth_config)
1872 for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001873 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001874 alignment = max(5, max(len(b) for b in branches))
1875 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001876 output = get_cl_statuses(branches,
1877 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001878 max_processes=options.maxjobs,
1879 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001880
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001881 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001882 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001883 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001884 while branch not in branch_statuses:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001885 b, i, status = output.next()
1886 branch_statuses[b] = (i, status)
1887 issue_url, status = branch_statuses.pop(branch)
1888 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001889 reset = Fore.RESET
1890 if not sys.stdout.isatty():
1891 color = ''
1892 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001893 status_str = '(%s)' % status if status else ''
1894 print ' %*s : %s%s %s%s' % (
1895 alignment, ShortBranchName(branch), color, issue_url, status_str,
1896 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001897
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001898 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001899 print
1900 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001901 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00001902 if not cl.GetIssue():
1903 print 'No issue assigned.'
1904 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001905 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00001906 if not options.fast:
1907 print 'Issue description:'
1908 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001909 return 0
1910
1911
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001912def colorize_CMDstatus_doc():
1913 """To be called once in main() to add colors to git cl status help."""
1914 colors = [i for i in dir(Fore) if i[0].isupper()]
1915
1916 def colorize_line(line):
1917 for color in colors:
1918 if color in line.upper():
1919 # Extract whitespaces first and the leading '-'.
1920 indent = len(line) - len(line.lstrip(' ')) + 1
1921 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1922 return line
1923
1924 lines = CMDstatus.__doc__.splitlines()
1925 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1926
1927
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001928@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001929def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001930 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001931
1932 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001933 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00001934 parser.add_option('-r', '--reverse', action='store_true',
1935 help='Lookup the branch(es) for the specified issues. If '
1936 'no issues are specified, all branches with mapped '
1937 'issues will be listed.')
1938 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001939
dnj@chromium.org406c4402015-03-03 17:22:28 +00001940 if options.reverse:
1941 branches = RunGit(['for-each-ref', 'refs/heads',
1942 '--format=%(refname:short)']).splitlines()
1943
1944 # Reverse issue lookup.
1945 issue_branch_map = {}
1946 for branch in branches:
1947 cl = Changelist(branchref=branch)
1948 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
1949 if not args:
1950 args = sorted(issue_branch_map.iterkeys())
1951 for issue in args:
1952 if not issue:
1953 continue
1954 print 'Branch for issue number %s: %s' % (
1955 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
1956 else:
1957 cl = Changelist()
1958 if len(args) > 0:
1959 try:
1960 issue = int(args[0])
1961 except ValueError:
1962 DieWithError('Pass a number to set the issue or none to list it.\n'
1963 'Maybe you want to run git cl status?')
1964 cl.SetIssue(issue)
1965 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001966 return 0
1967
1968
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001969def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001970 """Shows or posts review comments for any changelist."""
1971 parser.add_option('-a', '--add-comment', dest='comment',
1972 help='comment to add to an issue')
1973 parser.add_option('-i', dest='issue',
1974 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00001975 parser.add_option('-j', '--json-file',
1976 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001977 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001978 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001979 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001980
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001981 issue = None
1982 if options.issue:
1983 try:
1984 issue = int(options.issue)
1985 except ValueError:
1986 DieWithError('A review issue id is expected to be a number')
1987
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001988 cl = Changelist(issue=issue, auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001989
1990 if options.comment:
1991 cl.AddComment(options.comment)
1992 return 0
1993
1994 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00001995 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00001996 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00001997 summary.append({
1998 'date': message['date'],
1999 'lgtm': False,
2000 'message': message['text'],
2001 'not_lgtm': False,
2002 'sender': message['sender'],
2003 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002004 if message['disapproval']:
2005 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00002006 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002007 elif message['approval']:
2008 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00002009 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002010 elif message['sender'] == data['owner_email']:
2011 color = Fore.MAGENTA
2012 else:
2013 color = Fore.BLUE
2014 print '\n%s%s %s%s' % (
2015 color, message['date'].split('.', 1)[0], message['sender'],
2016 Fore.RESET)
2017 if message['text'].strip():
2018 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00002019 if options.json_file:
2020 with open(options.json_file, 'wb') as f:
2021 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002022 return 0
2023
2024
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002025def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002026 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00002027 parser.add_option('-d', '--display', action='store_true',
2028 help='Display the description instead of opening an editor')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002029 auth.add_auth_options(parser)
2030 options, _ = parser.parse_args(args)
2031 auth_config = auth.extract_auth_config_from_options(options)
2032 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002033 if not cl.GetIssue():
2034 DieWithError('This branch has no associated changelist.')
2035 description = ChangeDescription(cl.GetDescription())
smut@google.com34fb6b12015-07-13 20:03:26 +00002036 if options.display:
2037 print description.description
2038 return 0
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002039 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00002040 if cl.GetDescription() != description.description:
2041 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002042 return 0
2043
2044
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002045def CreateDescriptionFromLog(args):
2046 """Pulls out the commit log to use as a base for the CL description."""
2047 log_args = []
2048 if len(args) == 1 and not args[0].endswith('.'):
2049 log_args = [args[0] + '..']
2050 elif len(args) == 1 and args[0].endswith('...'):
2051 log_args = [args[0][:-1]]
2052 elif len(args) == 2:
2053 log_args = [args[0] + '..' + args[1]]
2054 else:
2055 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00002056 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002057
2058
thestig@chromium.org44202a22014-03-11 19:22:18 +00002059def CMDlint(parser, args):
2060 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002061 parser.add_option('--filter', action='append', metavar='-x,+y',
2062 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002063 auth.add_auth_options(parser)
2064 options, args = parser.parse_args(args)
2065 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002066
2067 # Access to a protected member _XX of a client class
2068 # pylint: disable=W0212
2069 try:
2070 import cpplint
2071 import cpplint_chromium
2072 except ImportError:
2073 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
2074 return 1
2075
2076 # Change the current working directory before calling lint so that it
2077 # shows the correct base.
2078 previous_cwd = os.getcwd()
2079 os.chdir(settings.GetRoot())
2080 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002081 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002082 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
2083 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00002084 if not files:
2085 print "Cannot lint an empty CL"
2086 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00002087
2088 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002089 command = args + files
2090 if options.filter:
2091 command = ['--filter=' + ','.join(options.filter)] + command
2092 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002093
2094 white_regex = re.compile(settings.GetLintRegex())
2095 black_regex = re.compile(settings.GetLintIgnoreRegex())
2096 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
2097 for filename in filenames:
2098 if white_regex.match(filename):
2099 if black_regex.match(filename):
2100 print "Ignoring file %s" % filename
2101 else:
2102 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
2103 extra_check_functions)
2104 else:
2105 print "Skipping file %s" % filename
2106 finally:
2107 os.chdir(previous_cwd)
2108 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
2109 if cpplint._cpplint_state.error_count != 0:
2110 return 1
2111 return 0
2112
2113
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002114def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002115 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002116 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002117 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002118 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00002119 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002120 auth.add_auth_options(parser)
2121 options, args = parser.parse_args(args)
2122 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002123
sbc@chromium.org71437c02015-04-09 19:29:40 +00002124 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00002125 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002126 return 1
2127
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002128 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002129 if args:
2130 base_branch = args[0]
2131 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002132 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002133 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002134
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002135 cl.RunHook(
2136 committing=not options.upload,
2137 may_prompt=False,
2138 verbose=options.verbose,
2139 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00002140 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002141
2142
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002143def AddChangeIdToCommitMessage(options, args):
2144 """Re-commits using the current message, assumes the commit hook is in
2145 place.
2146 """
2147 log_desc = options.message or CreateDescriptionFromLog(args)
2148 git_command = ['commit', '--amend', '-m', log_desc]
2149 RunGit(git_command)
2150 new_log_desc = CreateDescriptionFromLog(args)
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00002151 if git_footers.get_footer_change_id(new_log_desc):
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002152 print 'git-cl: Added Change-Id to commit message.'
2153 else:
2154 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
2155
2156
tandrii@chromium.org65874e12016-03-04 12:03:02 +00002157def GenerateGerritChangeId(message):
2158 """Returns Ixxxxxx...xxx change id.
2159
2160 Works the same way as
2161 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
2162 but can be called on demand on all platforms.
2163
2164 The basic idea is to generate git hash of a state of the tree, original commit
2165 message, author/committer info and timestamps.
2166 """
2167 lines = []
2168 tree_hash = RunGitSilent(['write-tree'])
2169 lines.append('tree %s' % tree_hash.strip())
2170 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
2171 if code == 0:
2172 lines.append('parent %s' % parent.strip())
2173 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
2174 lines.append('author %s' % author.strip())
2175 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
2176 lines.append('committer %s' % committer.strip())
2177 lines.append('')
2178 # Note: Gerrit's commit-hook actually cleans message of some lines and
2179 # whitespace. This code is not doing this, but it clearly won't decrease
2180 # entropy.
2181 lines.append(message)
2182 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
2183 stdin='\n'.join(lines))
2184 return 'I%s' % change_hash.strip()
2185
2186
piman@chromium.org336f9122014-09-04 02:16:55 +00002187def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002188 """upload the current branch to gerrit."""
2189 # We assume the remote called "origin" is the one we want.
2190 # It is probably not worthwhile to support different workflows.
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002191 gerrit_remote = 'origin'
luqui@chromium.org609f3952015-05-04 22:47:04 +00002192
2193 remote, remote_branch = cl.GetRemoteBranch()
2194 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2195 pending_prefix='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002196
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002197 change_desc = ChangeDescription(
2198 options.message or CreateDescriptionFromLog(args))
2199 if not change_desc.description:
andybons@chromium.org962f9462016-02-03 20:00:42 +00002200 print "\nDescription is empty. Aborting..."
2201 return 1
2202
2203 if options.title:
2204 print "\nPatch titles (-t) are not supported in Gerrit. Aborting..."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002205 return 1
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002206
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002207 if options.squash:
2208 # Try to get the message from a previous upload.
2209 shadow_branch = 'refs/heads/git_cl_uploads/' + cl.GetBranch()
bauerb@chromium.org13502e02016-02-18 10:18:29 +00002210 message = RunGitSilent(['show', '--format=%B', '-s', shadow_branch])
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002211 if not message:
2212 if not options.force:
2213 change_desc.prompt()
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002214 if not change_desc.description:
2215 print "Description is empty; aborting."
2216 return 1
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002217 message = change_desc.description
tandrii@chromium.org57d86542016-03-04 16:11:32 +00002218 change_ids = git_footers.get_footer_change_id(message)
2219 if len(change_ids) > 1:
2220 DieWithError('too many Change-Id footers in %s branch' % shadow_branch)
2221 if not change_ids:
2222 message = git_footers.add_footer_change_id(
2223 message, GenerateGerritChangeId(message))
2224 change_desc.set_description(message)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002225
2226 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2227 if remote is '.':
2228 # If our upstream branch is local, we base our squashed commit on its
2229 # squashed version.
2230 parent = ('refs/heads/git_cl_uploads/' +
2231 scm.GIT.ShortBranchName(upstream_branch))
2232
2233 # Verify that the upstream branch has been uploaded too, otherwise Gerrit
2234 # will create additional CLs when uploading.
2235 if (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2236 RunGitSilent(['rev-parse', parent + ':'])):
2237 print 'Upload upstream branch ' + upstream_branch + ' first.'
2238 return 1
2239 else:
2240 parent = cl.GetCommonAncestorWithUpstream()
2241
2242 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2243 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2244 '-m', message]).strip()
2245 else:
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00002246 if not git_footers.get_footer_change_id(change_desc.description):
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002247 AddChangeIdToCommitMessage(options, args)
2248 ref_to_push = 'HEAD'
2249 parent = '%s/%s' % (gerrit_remote, branch)
2250
2251 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2252 ref_to_push)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002253 if len(commits) > 1:
2254 print('WARNING: This will upload %d commits. Run the following command '
2255 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002256 print('git log %s..%s' % (parent, ref_to_push))
2257 print('You can also use `git squash-branch` to squash these into a single '
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002258 'commit.')
2259 ask_for_data('About to upload; enter to confirm.')
2260
piman@chromium.org336f9122014-09-04 02:16:55 +00002261 if options.reviewers or options.tbr_owners:
2262 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002263
ukai@chromium.orge8077812012-02-03 03:41:46 +00002264 receive_options = []
2265 cc = cl.GetCCList().split(',')
2266 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002267 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002268 cc = filter(None, cc)
2269 if cc:
2270 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002271 if change_desc.get_reviewers():
2272 receive_options.extend(
2273 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002274
ukai@chromium.orge8077812012-02-03 03:41:46 +00002275 git_command = ['push']
2276 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00002277 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00002278 ' '.join(receive_options))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002279 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
ukai@chromium.orge8077812012-02-03 03:41:46 +00002280 RunGit(git_command)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002281
2282 if options.squash:
2283 head = RunGit(['rev-parse', 'HEAD']).strip()
2284 RunGit(['update-ref', '-m', 'Uploaded ' + head, shadow_branch, ref_to_push])
2285
ukai@chromium.orge8077812012-02-03 03:41:46 +00002286 # TODO(ukai): parse Change-Id: and set issue number?
2287 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002288
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002289
wittman@chromium.org455dc922015-01-26 20:15:50 +00002290def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
2291 """Computes the remote branch ref to use for the CL.
2292
2293 Args:
2294 remote (str): The git remote for the CL.
2295 remote_branch (str): The git remote branch for the CL.
2296 target_branch (str): The target branch specified by the user.
2297 pending_prefix (str): The pending prefix from the settings.
2298 """
2299 if not (remote and remote_branch):
2300 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002301
wittman@chromium.org455dc922015-01-26 20:15:50 +00002302 if target_branch:
2303 # Cannonicalize branch references to the equivalent local full symbolic
2304 # refs, which are then translated into the remote full symbolic refs
2305 # below.
2306 if '/' not in target_branch:
2307 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
2308 else:
2309 prefix_replacements = (
2310 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
2311 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
2312 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
2313 )
2314 match = None
2315 for regex, replacement in prefix_replacements:
2316 match = re.search(regex, target_branch)
2317 if match:
2318 remote_branch = target_branch.replace(match.group(0), replacement)
2319 break
2320 if not match:
2321 # This is a branch path but not one we recognize; use as-is.
2322 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00002323 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
2324 # Handle the refs that need to land in different refs.
2325 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002326
wittman@chromium.org455dc922015-01-26 20:15:50 +00002327 # Create the true path to the remote branch.
2328 # Does the following translation:
2329 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
2330 # * refs/remotes/origin/master -> refs/heads/master
2331 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
2332 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
2333 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
2334 elif remote_branch.startswith('refs/remotes/%s/' % remote):
2335 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
2336 'refs/heads/')
2337 elif remote_branch.startswith('refs/remotes/branch-heads'):
2338 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
2339 # If a pending prefix exists then replace refs/ with it.
2340 if pending_prefix:
2341 remote_branch = remote_branch.replace('refs/', pending_prefix)
2342 return remote_branch
2343
2344
piman@chromium.org336f9122014-09-04 02:16:55 +00002345def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002346 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002347 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2348 upload_args.extend(['--server', cl.GetRietveldServer()])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002349 upload_args.extend(auth.auth_config_to_command_options(cl.auth_config))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002350 if options.emulate_svn_auto_props:
2351 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002352
2353 change_desc = None
2354
pgervais@chromium.org91141372014-01-09 23:27:20 +00002355 if options.email is not None:
2356 upload_args.extend(['--email', options.email])
2357
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002358 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002359 if options.title:
2360 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00002361 if options.message:
2362 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00002363 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002364 print ("This branch is associated with issue %s. "
2365 "Adding patch to that issue." % cl.GetIssue())
2366 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002367 if options.title:
2368 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00002369 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002370 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00002371 if options.reviewers or options.tbr_owners:
2372 change_desc.update_reviewers(options.reviewers,
2373 options.tbr_owners,
2374 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002375 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002376 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002377
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002378 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002379 print "Description is empty; aborting."
2380 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002381
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002382 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002383 if change_desc.get_reviewers():
2384 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00002385 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002386 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00002387 DieWithError("Must specify reviewers to send email.")
2388 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00002389
2390 # We check this before applying rietveld.private assuming that in
2391 # rietveld.cc only addresses which we can send private CLs to are listed
2392 # if rietveld.private is set, and so we should ignore rietveld.cc only when
2393 # --private is specified explicitly on the command line.
2394 if options.private:
2395 logging.warn('rietveld.cc is ignored since private flag is specified. '
2396 'You need to review and add them manually if necessary.')
2397 cc = cl.GetCCListWithoutDefault()
2398 else:
2399 cc = cl.GetCCList()
2400 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00002401 if cc:
2402 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002403
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002404 if options.private or settings.GetDefaultPrivateFlag() == "True":
2405 upload_args.append('--private')
2406
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002407 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00002408 if not options.find_copies:
2409 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002410
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002411 # Include the upstream repo's URL in the change -- this is useful for
2412 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002413 remote_url = cl.GetGitBaseUrlFromConfig()
2414 if not remote_url:
2415 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002416 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002417 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00002418 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
2419 remote_url = (cl.GetRemoteUrl() + '@'
2420 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002421 if remote_url:
2422 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00002423 remote, remote_branch = cl.GetRemoteBranch()
wittman@chromium.org455dc922015-01-26 20:15:50 +00002424 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2425 settings.GetPendingRefPrefix())
2426 if target_ref:
2427 upload_args.extend(['--target_ref', target_ref])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002428
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002429 # Look for dependent patchsets. See crbug.com/480453 for more details.
2430 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2431 upstream_branch = ShortBranchName(upstream_branch)
2432 if remote is '.':
2433 # A local branch is being tracked.
2434 local_branch = ShortBranchName(upstream_branch)
rmistry@google.com78948ed2015-07-08 23:09:57 +00002435 if settings.GetIsSkipDependencyUpload(local_branch):
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002436 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002437 print ('Skipping dependency patchset upload because git config '
2438 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002439 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002440 else:
2441 auth_config = auth.extract_auth_config_from_options(options)
2442 branch_cl = Changelist(branchref=local_branch, auth_config=auth_config)
2443 branch_cl_issue_url = branch_cl.GetIssueURL()
2444 branch_cl_issue = branch_cl.GetIssue()
2445 branch_cl_patchset = branch_cl.GetPatchset()
2446 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2447 upload_args.extend(
2448 ['--depends_on_patchset', '%s:%s' % (
2449 branch_cl_issue, branch_cl_patchset)])
2450 print
2451 print ('The current branch (%s) is tracking a local branch (%s) with '
2452 'an associated CL.') % (cl.GetBranch(), local_branch)
2453 print 'Adding %s/#ps%s as a dependency patchset.' % (
2454 branch_cl_issue_url, branch_cl_patchset)
2455 print
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002456
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002457 project = settings.GetProject()
2458 if project:
2459 upload_args.extend(['--project', project])
2460
rmistry@google.comef966222015-04-07 11:15:01 +00002461 if options.cq_dry_run:
2462 upload_args.extend(['--cq_dry_run'])
2463
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002464 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00002465 upload_args = ['upload'] + upload_args + args
2466 logging.info('upload.RealMain(%s)', upload_args)
2467 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00002468 issue = int(issue)
2469 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00002470 except KeyboardInterrupt:
2471 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002472 except:
2473 # If we got an exception after the user typed a description for their
2474 # change, back up the description before re-raising.
2475 if change_desc:
2476 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2477 print '\nGot exception while uploading -- saving description to %s\n' \
2478 % backup_path
2479 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002480 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002481 backup_file.close()
2482 raise
2483
2484 if not cl.GetIssue():
2485 cl.SetIssue(issue)
2486 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002487
2488 if options.use_commit_queue:
2489 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002490 return 0
2491
2492
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002493def cleanup_list(l):
2494 """Fixes a list so that comma separated items are put as individual items.
2495
2496 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
2497 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
2498 """
2499 items = sum((i.split(',') for i in l), [])
2500 stripped_items = (i.strip() for i in items)
2501 return sorted(filter(None, stripped_items))
2502
2503
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002504@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002505def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00002506 """Uploads the current changelist to codereview.
2507
2508 Can skip dependency patchset uploads for a branch by running:
2509 git config branch.branch_name.skip-deps-uploads True
2510 To unset run:
2511 git config --unset branch.branch_name.skip-deps-uploads
2512 Can also set the above globally by using the --global flag.
2513 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00002514 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2515 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002516 parser.add_option('--bypass-watchlists', action='store_true',
2517 dest='bypass_watchlists',
2518 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002519 parser.add_option('-f', action='store_true', dest='force',
2520 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002521 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00002522 parser.add_option('-t', dest='title',
2523 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002524 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002525 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002526 help='reviewer email addresses')
2527 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002528 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002529 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00002530 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00002531 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002532 parser.add_option('--emulate_svn_auto_props',
2533 '--emulate-svn-auto-props',
2534 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00002535 dest="emulate_svn_auto_props",
2536 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00002537 parser.add_option('-c', '--use-commit-queue', action='store_true',
2538 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002539 parser.add_option('--private', action='store_true',
2540 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00002541 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002542 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00002543 metavar='TARGET',
2544 help='Apply CL to remote ref TARGET. ' +
2545 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002546 parser.add_option('--squash', action='store_true',
2547 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002548 parser.add_option('--no-squash', action='store_true',
2549 help='Don\'t squash multiple commits into one ' +
2550 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002551 parser.add_option('--email', default=None,
2552 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00002553 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
2554 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00002555 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
2556 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00002557 help='Send the patchset to do a CQ dry run right after '
2558 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00002559 parser.add_option('--dependencies', action='store_true',
2560 help='Uploads CLs of all the local branches that depend on '
2561 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002562
rmistry@google.com2dd99862015-06-22 12:22:18 +00002563 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002564 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002565 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002566 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002567 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002568
sbc@chromium.org71437c02015-04-09 19:29:40 +00002569 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002570 return 1
2571
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002572 options.reviewers = cleanup_list(options.reviewers)
2573 options.cc = cleanup_list(options.cc)
2574
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002575 cl = Changelist(auth_config=auth_config)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002576 if args:
2577 # TODO(ukai): is it ok for gerrit case?
2578 base_branch = args[0]
2579 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00002580 if cl.GetBranch() is None:
2581 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
2582
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002583 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002584 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00002585 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00002586
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00002587 # Make sure authenticated to Rietveld before running expensive hooks. It is
2588 # a fast, best efforts check. Rietveld still can reject the authentication
2589 # during the actual upload.
2590 if not settings.GetIsGerrit() and auth_config.use_oauth2:
2591 authenticator = auth.get_authenticator_for_host(
2592 cl.GetRietveldServer(), auth_config)
2593 if not authenticator.has_cached_credentials():
2594 raise auth.LoginRequiredError(cl.GetRietveldServer())
2595
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002596 # Apply watchlists on upload.
2597 change = cl.GetChange(base_branch, None)
2598 watchlist = watchlists.Watchlists(change.RepositoryRoot())
2599 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002600 if not options.bypass_watchlists:
2601 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002602
ukai@chromium.orge8077812012-02-03 03:41:46 +00002603 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00002604 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002605 # Set the reviewer list now so that presubmit checks can access it.
2606 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00002607 change_description.update_reviewers(options.reviewers,
2608 options.tbr_owners,
2609 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002610 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002611 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00002612 may_prompt=not options.force,
2613 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002614 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002615 if not hook_results.should_continue():
2616 return 1
2617 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002618 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002619
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002620 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002621 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002622 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00002623 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002624 print ('The last upload made from this repository was patchset #%d but '
2625 'the most recent patchset on the server is #%d.'
2626 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00002627 print ('Uploading will still work, but if you\'ve uploaded to this issue '
2628 'from another machine or branch the patch you\'re uploading now '
2629 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002630 ask_for_data('About to upload; enter to confirm.')
2631
iannucci@chromium.org79540052012-10-19 23:15:26 +00002632 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002633 if settings.GetIsGerrit():
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002634 if options.squash and options.no_squash:
2635 DieWithError('Can only use one of --squash or --no-squash')
2636
2637 options.squash = ((settings.GetSquashGerritUploads() or options.squash) and
2638 not options.no_squash)
2639
tandrii@chromium.org1e67bb72016-02-11 12:15:49 +00002640 ret = GerritUpload(options, args, cl, change)
2641 else:
2642 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002643 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00002644 git_set_branch_value('last-upload-hash',
2645 RunGit(['rev-parse', 'HEAD']).strip())
rmistry@google.com5626a922015-02-26 14:03:30 +00002646 # Run post upload hooks, if specified.
2647 if settings.GetRunPostUploadHook():
2648 presubmit_support.DoPostUploadExecuter(
2649 change,
2650 cl,
2651 settings.GetRoot(),
2652 options.verbose,
2653 sys.stdout)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002654
rmistry@google.com2dd99862015-06-22 12:22:18 +00002655 # Upload all dependencies if specified.
2656 if options.dependencies:
2657 print
2658 print '--dependencies has been specified.'
2659 print 'All dependent local branches will be re-uploaded.'
2660 print
2661 # Remove the dependencies flag from args so that we do not end up in a
2662 # loop.
2663 orig_args.remove('--dependencies')
2664 upload_branch_deps(cl, orig_args)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002665 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00002666
2667
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002668def IsSubmoduleMergeCommit(ref):
2669 # When submodules are added to the repo, we expect there to be a single
2670 # non-git-svn merge commit at remote HEAD with a signature comment.
2671 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00002672 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002673 return RunGit(cmd) != ''
2674
2675
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002676def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002677 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002678
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002679 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002680 Updates changelog with metadata (e.g. pointer to review).
2681 Pushes/dcommits the code upstream.
2682 Updates review and closes.
2683 """
2684 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2685 help='bypass upload presubmit hook')
2686 parser.add_option('-m', dest='message',
2687 help="override review description")
2688 parser.add_option('-f', action='store_true', dest='force',
2689 help="force yes to questions (don't prompt)")
2690 parser.add_option('-c', dest='contributor',
2691 help="external contributor for patch (appended to " +
2692 "description and used as author for git). Should be " +
2693 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002694 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002695 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002696 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002697 auth_config = auth.extract_auth_config_from_options(options)
2698
2699 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002700
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002701 current = cl.GetBranch()
2702 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2703 if not settings.GetIsGitSvn() and remote == '.':
2704 print
2705 print 'Attempting to push branch %r into another local branch!' % current
2706 print
2707 print 'Either reparent this branch on top of origin/master:'
2708 print ' git reparent-branch --root'
2709 print
2710 print 'OR run `git rebase-update` if you think the parent branch is already'
2711 print 'committed.'
2712 print
2713 print ' Current parent: %r' % upstream_branch
2714 return 1
2715
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002716 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002717 # Default to merging against our best guess of the upstream branch.
2718 args = [cl.GetUpstreamBranch()]
2719
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002720 if options.contributor:
2721 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
2722 print "Please provide contibutor as 'First Last <email@example.com>'"
2723 return 1
2724
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002725 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002726 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002727
sbc@chromium.org71437c02015-04-09 19:29:40 +00002728 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002729 return 1
2730
2731 # This rev-list syntax means "show all commits not in my branch that
2732 # are in base_branch".
2733 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
2734 base_branch]).splitlines()
2735 if upstream_commits:
2736 print ('Base branch "%s" has %d commits '
2737 'not in this branch.' % (base_branch, len(upstream_commits)))
2738 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
2739 return 1
2740
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002741 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002742 svn_head = None
2743 if cmd == 'dcommit' or base_has_submodules:
2744 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
2745 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002746
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002747 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002748 # If the base_head is a submodule merge commit, the first parent of the
2749 # base_head should be a git-svn commit, which is what we're interested in.
2750 base_svn_head = base_branch
2751 if base_has_submodules:
2752 base_svn_head += '^1'
2753
2754 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002755 if extra_commits:
2756 print ('This branch has %d additional commits not upstreamed yet.'
2757 % len(extra_commits.splitlines()))
2758 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
2759 'before attempting to %s.' % (base_branch, cmd))
2760 return 1
2761
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002762 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002763 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002764 author = None
2765 if options.contributor:
2766 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002767 hook_results = cl.RunHook(
2768 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002769 may_prompt=not options.force,
2770 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002771 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002772 if not hook_results.should_continue():
2773 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002774
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002775 # Check the tree status if the tree status URL is set.
2776 status = GetTreeStatus()
2777 if 'closed' == status:
2778 print('The tree is closed. Please wait for it to reopen. Use '
2779 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2780 return 1
2781 elif 'unknown' == status:
2782 print('Unable to determine tree status. Please verify manually and '
2783 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2784 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002785
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002786 change_desc = ChangeDescription(options.message)
2787 if not change_desc.description and cl.GetIssue():
2788 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002789
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002790 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00002791 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002792 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00002793 else:
2794 print 'No description set.'
2795 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
2796 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002797
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002798 # Keep a separate copy for the commit message, because the commit message
2799 # contains the link to the Rietveld issue, while the Rietveld message contains
2800 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002801 # Keep a separate copy for the commit message.
2802 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00002803 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002804
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002805 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00002806 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00002807 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00002808 # after it. Add a period on a new line to circumvent this. Also add a space
2809 # before the period to make sure that Gitiles continues to correctly resolve
2810 # the URL.
2811 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002812 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002813 commit_desc.append_footer('Patch from %s.' % options.contributor)
2814
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00002815 print('Description:')
2816 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002817
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002818 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002819 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00002820 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002821
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002822 # We want to squash all this branch's commits into one commit with the proper
2823 # description. We do this by doing a "reset --soft" to the base branch (which
2824 # keeps the working copy the same), then dcommitting that. If origin/master
2825 # has a submodule merge commit, we'll also need to cherry-pick the squashed
2826 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002827 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002828 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
2829 # Delete the branches if they exist.
2830 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
2831 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
2832 result = RunGitWithCode(showref_cmd)
2833 if result[0] == 0:
2834 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002835
2836 # We might be in a directory that's present in this branch but not in the
2837 # trunk. Move up to the top of the tree so that git commands that expect a
2838 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002839 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002840 if rel_base_path:
2841 os.chdir(rel_base_path)
2842
2843 # Stuff our change into the merge branch.
2844 # We wrap in a try...finally block so if anything goes wrong,
2845 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002846 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002847 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002848 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002849 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002850 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00002851 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002852 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002853 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002854 RunGit(
2855 [
2856 'commit', '--author', options.contributor,
2857 '-m', commit_desc.description,
2858 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002859 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002860 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002861 if base_has_submodules:
2862 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
2863 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
2864 RunGit(['checkout', CHERRY_PICK_BRANCH])
2865 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002866 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002867 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002868 pending_prefix = settings.GetPendingRefPrefix()
2869 if not pending_prefix or branch.startswith(pending_prefix):
2870 # If not using refs/pending/heads/* at all, or target ref is already set
2871 # to pending, then push to the target ref directly.
2872 retcode, output = RunGitWithCode(
2873 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002874 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002875 else:
2876 # Cherry-pick the change on top of pending ref and then push it.
2877 assert branch.startswith('refs/'), branch
2878 assert pending_prefix[-1] == '/', pending_prefix
2879 pending_ref = pending_prefix + branch[len('refs/'):]
2880 retcode, output = PushToGitPending(remote, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002881 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002882 if retcode == 0:
2883 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002884 else:
2885 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002886 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002887 'svn', 'dcommit',
2888 '-C%s' % options.similarity,
2889 '--no-rebase', '--rmdir',
2890 ]
2891 if settings.GetForceHttpsCommitUrl():
2892 # Allow forcing https commit URLs for some projects that don't allow
2893 # committing to http URLs (like Google Code).
2894 remote_url = cl.GetGitSvnRemoteUrl()
2895 if urlparse.urlparse(remote_url).scheme == 'http':
2896 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002897 cmd_args.append('--commit-url=%s' % remote_url)
2898 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002899 if 'Committed r' in output:
2900 revision = re.match(
2901 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2902 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002903 finally:
2904 # And then swap back to the original branch and clean up.
2905 RunGit(['checkout', '-q', cl.GetBranch()])
2906 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002907 if base_has_submodules:
2908 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002909
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002910 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002911 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002912 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002913
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002914 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002915 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002916 try:
2917 revision = WaitForRealCommit(remote, revision, base_branch, branch)
2918 # We set pushed_to_pending to False, since it made it all the way to the
2919 # real ref.
2920 pushed_to_pending = False
2921 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002922 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002923
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002924 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002925 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002926 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002927 if not to_pending:
2928 if viewvc_url and revision:
2929 change_desc.append_footer(
2930 'Committed: %s%s' % (viewvc_url, revision))
2931 elif revision:
2932 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002933 print ('Closing issue '
2934 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002935 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002936 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002937 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002938 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00002939 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00002940 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002941 if options.bypass_hooks:
2942 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2943 else:
2944 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002945 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002946 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002947
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002948 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002949 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
2950 print 'The commit is in the pending queue (%s).' % pending_ref
2951 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00002952 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002953 'footer.' % branch)
2954
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002955 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2956 if os.path.isfile(hook):
2957 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002958
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002959 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002960
2961
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002962def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
2963 print
2964 print 'Waiting for commit to be landed on %s...' % real_ref
2965 print '(If you are impatient, you may Ctrl-C once without harm)'
2966 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
2967 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
2968
2969 loop = 0
2970 while True:
2971 sys.stdout.write('fetching (%d)... \r' % loop)
2972 sys.stdout.flush()
2973 loop += 1
2974
2975 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
2976 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2977 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
2978 for commit in commits.splitlines():
2979 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
2980 print 'Found commit on %s' % real_ref
2981 return commit
2982
2983 current_rev = to_rev
2984
2985
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002986def PushToGitPending(remote, pending_ref, upstream_ref):
2987 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
2988
2989 Returns:
2990 (retcode of last operation, output log of last operation).
2991 """
2992 assert pending_ref.startswith('refs/'), pending_ref
2993 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
2994 cherry = RunGit(['rev-parse', 'HEAD']).strip()
2995 code = 0
2996 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002997 max_attempts = 3
2998 attempts_left = max_attempts
2999 while attempts_left:
3000 if attempts_left != max_attempts:
3001 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
3002 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003003
3004 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003005 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003006 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003007 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003008 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003009 print 'Fetch failed with exit code %d.' % code
3010 if out.strip():
3011 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003012 continue
3013
3014 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003015 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003016 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003017 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003018 if code:
3019 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003020 'Your patch doesn\'t apply cleanly to ref \'%s\', '
3021 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003022 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
3023 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003024 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003025 return code, out
3026
3027 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003028 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003029 code, out = RunGitWithCode(
3030 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
3031 if code == 0:
3032 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003033 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003034 return code, out
3035
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003036 print 'Push failed with exit code %d.' % code
3037 if out.strip():
3038 print out.strip()
3039 if IsFatalPushFailure(out):
3040 print (
3041 'Fatal push error. Make sure your .netrc credentials and git '
3042 'user.email are correct and you have push access to the repo.')
3043 return code, out
3044
3045 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003046 return code, out
3047
3048
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003049def IsFatalPushFailure(push_stdout):
3050 """True if retrying push won't help."""
3051 return '(prohibited by Gerrit)' in push_stdout
3052
3053
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003054@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003055def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003056 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003057 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003058 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003059 # If it looks like previous commits were mirrored with git-svn.
3060 message = """This repository appears to be a git-svn mirror, but no
3061upstream SVN master is set. You probably need to run 'git auto-svn' once."""
3062 else:
3063 message = """This doesn't appear to be an SVN repository.
3064If your project has a true, writeable git repository, you probably want to run
3065'git cl land' instead.
3066If your project has a git mirror of an upstream SVN master, you probably need
3067to run 'git svn init'.
3068
3069Using the wrong command might cause your commit to appear to succeed, and the
3070review to be closed, without actually landing upstream. If you choose to
3071proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00003072 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00003073 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003074 return SendUpstream(parser, args, 'dcommit')
3075
3076
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003077@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003078def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003079 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003080 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003081 print('This appears to be an SVN repository.')
3082 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003083 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00003084 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003085 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003086
3087
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003088def ParseIssueNum(arg):
3089 """Parses the issue number from args if present otherwise returns None."""
3090 if re.match(r'\d+', arg):
3091 return arg
3092 if arg.startswith('http'):
3093 return re.sub(r'.*/(\d+)/?', r'\1', arg)
3094 return None
3095
3096
3097@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003098def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00003099 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003100 parser.add_option('-b', dest='newbranch',
3101 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003102 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003103 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003104 parser.add_option('-d', '--directory', action='store', metavar='DIR',
3105 help='Change to the directory DIR immediately, '
3106 'before doing anything else.')
3107 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003108 help='failed patches spew .rej files rather than '
3109 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003110 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
3111 help="don't commit after patch applies")
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003112
3113 group = optparse.OptionGroup(parser,
3114 """Options for continuing work on the current issue uploaded
3115from a different clone (e.g. different machine). Must be used independently from
3116the other options. No issue number should be specified, and the branch must have
3117an issue number associated with it""")
3118 group.add_option('--reapply', action='store_true',
3119 dest='reapply',
3120 help="""Reset the branch and reapply the issue.
3121CAUTION: This will undo any local changes in this branch""")
3122
3123 group.add_option('--pull', action='store_true', dest='pull',
3124 help="Performs a pull before reapplying.")
3125 parser.add_option_group(group)
3126
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003127 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003128 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003129 auth_config = auth.extract_auth_config_from_options(options)
3130
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003131 issue_arg = None
3132 if options.reapply :
3133 if len(args) > 0:
3134 parser.error("--reapply implies no additional arguments.")
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003135
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003136 cl = Changelist()
3137 issue_arg = cl.GetIssue()
3138 upstream = cl.GetUpstreamBranch()
3139 if upstream == None:
3140 parser.error("No upstream branch specified. Cannot reset branch")
3141
3142 RunGit(['reset', '--hard', upstream])
3143 if options.pull:
3144 RunGit(['pull'])
3145 else:
3146 if len(args) != 1:
3147 parser.error("Must specify issue number")
3148
3149 issue_arg = ParseIssueNum(args[0])
3150
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003151 # The patch URL works because ParseIssueNum won't do any substitution
3152 # as the re.sub pattern fails to match and just returns it.
3153 if issue_arg == None:
3154 parser.print_help()
3155 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003156
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003157 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003158 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003159 return 1
3160
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003161 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00003162 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003163
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003164 if options.newbranch:
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003165 if options.reapply:
3166 parser.error("--reapply excludes any option other than --pull")
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003167 if options.force:
3168 RunGit(['branch', '-D', options.newbranch],
3169 stderr=subprocess2.PIPE, error_ok=True)
3170 RunGit(['checkout', '-b', options.newbranch,
3171 Changelist().GetUpstreamBranch()])
3172
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003173 return PatchIssue(issue_arg, options.reject, options.nocommit,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003174 options.directory, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003175
3176
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003177def PatchIssue(issue_arg, reject, nocommit, directory, auth_config):
wychen@chromium.orga872e752015-04-28 23:42:18 +00003178 # PatchIssue should never be called with a dirty tree. It is up to the
3179 # caller to check this, but just in case we assert here since the
3180 # consequences of the caller not checking this could be dire.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003181 assert(not git_common.is_dirty_git_tree('apply'))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003182
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003183 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003184 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00003185 issue = int(issue_arg)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003186 cl = Changelist(issue=issue, auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003187 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00003188 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003189 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00003190 # Assume it's a URL to the patch. Default to https.
3191 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
kjellander@chromium.org44424542015-06-02 18:35:29 +00003192 match = re.match(r'(.*?)/download/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00003193 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003194 DieWithError('Must pass an issue ID or full URL for '
3195 '\'Download raw patch set\'')
kjellander@chromium.org44424542015-06-02 18:35:29 +00003196 issue = int(match.group(2))
3197 cl = Changelist(issue=issue, auth_config=auth_config)
3198 cl.rietveld_server = match.group(1)
3199 patchset = int(match.group(3))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00003200 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003201
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003202 # Switch up to the top-level directory, if necessary, in preparation for
3203 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003204 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003205 if top:
3206 os.chdir(top)
3207
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003208 # Git patches have a/ at the beginning of source paths. We strip that out
3209 # with a sed script rather than the -p flag to patch so we can feed either
3210 # Git or svn-style patches into the same apply command.
3211 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003212 try:
3213 patch_data = subprocess2.check_output(
3214 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
3215 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003216 DieWithError('Git patch mungling failed.')
3217 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003218
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003219 # We use "git apply" to apply the patch instead of "patch" so that we can
3220 # pick up file adds.
3221 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003222 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003223 if directory:
3224 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003225 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003226 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003227 elif IsGitVersionAtLeast('1.7.12'):
3228 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003229 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003230 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003231 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003232 except subprocess2.CalledProcessError:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003233 print 'Failed to apply the patch'
3234 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003235
3236 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003237 if not nocommit:
wychen@chromium.org5b3bebb2015-05-28 21:41:43 +00003238 RunGit(['commit', '-m', (cl.GetDescription() + '\n\n' +
3239 'patch from issue %(i)s at patchset '
carlosk@chromium.org71284d92014-11-14 18:12:50 +00003240 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
3241 % {'i': issue, 'p': patchset})])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003242 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003243 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00003244 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00003245 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003246 else:
3247 print "Patch applied to index."
3248 return 0
3249
3250
3251def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003252 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003253 # Provide a wrapper for git svn rebase to help avoid accidental
3254 # git svn dcommit.
3255 # It's the only command that doesn't use parser at all since we just defer
3256 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003257
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003258 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003259
3260
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003261def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003262 """Fetches the tree status and returns either 'open', 'closed',
3263 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003264 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003265 if url:
3266 status = urllib2.urlopen(url).read().lower()
3267 if status.find('closed') != -1 or status == '0':
3268 return 'closed'
3269 elif status.find('open') != -1 or status == '1':
3270 return 'open'
3271 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003272 return 'unset'
3273
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003274
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003275def GetTreeStatusReason():
3276 """Fetches the tree status from a json url and returns the message
3277 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00003278 url = settings.GetTreeStatusUrl()
3279 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003280 connection = urllib2.urlopen(json_url)
3281 status = json.loads(connection.read())
3282 connection.close()
3283 return status['message']
3284
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003285
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003286def GetBuilderMaster(bot_list):
3287 """For a given builder, fetch the master from AE if available."""
3288 map_url = 'https://builders-map.appspot.com/'
3289 try:
3290 master_map = json.load(urllib2.urlopen(map_url))
3291 except urllib2.URLError as e:
3292 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
3293 (map_url, e))
3294 except ValueError as e:
3295 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
3296 if not master_map:
3297 return None, 'Failed to build master map.'
3298
3299 result_master = ''
3300 for bot in bot_list:
3301 builder = bot.split(':', 1)[0]
3302 master_list = master_map.get(builder, [])
3303 if not master_list:
3304 return None, ('No matching master for builder %s.' % builder)
3305 elif len(master_list) > 1:
3306 return None, ('The builder name %s exists in multiple masters %s.' %
3307 (builder, master_list))
3308 else:
3309 cur_master = master_list[0]
3310 if not result_master:
3311 result_master = cur_master
3312 elif result_master != cur_master:
3313 return None, 'The builders do not belong to the same master.'
3314 return result_master, None
3315
3316
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003317def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003318 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003319 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003320 status = GetTreeStatus()
3321 if 'unset' == status:
3322 print 'You must configure your tree status URL by running "git cl config".'
3323 return 2
3324
3325 print "The tree is %s" % status
3326 print
3327 print GetTreeStatusReason()
3328 if status != 'open':
3329 return 1
3330 return 0
3331
3332
maruel@chromium.org15192402012-09-06 12:38:29 +00003333def CMDtry(parser, args):
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003334 """Triggers a try job through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00003335 group = optparse.OptionGroup(parser, "Try job options")
3336 group.add_option(
3337 "-b", "--bot", action="append",
3338 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
3339 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003340 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00003341 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003342 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00003343 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003344 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00003345 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003346 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003347 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003348 "-r", "--revision",
3349 help="Revision to use for the try job; default: the "
3350 "revision will be determined by the try server; see "
3351 "its waterfall for more info")
3352 group.add_option(
3353 "-c", "--clobber", action="store_true", default=False,
3354 help="Force a clobber before building; e.g. don't do an "
3355 "incremental build")
3356 group.add_option(
3357 "--project",
3358 help="Override which project to use. Projects are defined "
3359 "server-side to define what default bot set to use")
3360 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00003361 "-p", "--property", dest="properties", action="append", default=[],
3362 help="Specify generic properties in the form -p key1=value1 -p "
3363 "key2=value2 etc (buildbucket only). The value will be treated as "
3364 "json if decodable, or as string otherwise.")
3365 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003366 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003367 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003368 "--use-rietveld", action="store_true", default=False,
3369 help="Use Rietveld to trigger try jobs.")
3370 group.add_option(
3371 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3372 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00003373 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003374 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00003375 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003376 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00003377
machenbach@chromium.org45453142015-09-15 08:45:22 +00003378 if options.use_rietveld and options.properties:
3379 parser.error('Properties can only be specified with buildbucket')
3380
3381 # Make sure that all properties are prop=value pairs.
3382 bad_params = [x for x in options.properties if '=' not in x]
3383 if bad_params:
3384 parser.error('Got properties with missing "=": %s' % bad_params)
3385
maruel@chromium.org15192402012-09-06 12:38:29 +00003386 if args:
3387 parser.error('Unknown arguments: %s' % args)
3388
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003389 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00003390 if not cl.GetIssue():
3391 parser.error('Need to upload first')
3392
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003393 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00003394 if props.get('closed'):
3395 parser.error('Cannot send tryjobs for a closed CL')
3396
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003397 if props.get('private'):
3398 parser.error('Cannot use trybots with private issue')
3399
maruel@chromium.org15192402012-09-06 12:38:29 +00003400 if not options.name:
3401 options.name = cl.GetBranch()
3402
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003403 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003404 options.master, err_msg = GetBuilderMaster(options.bot)
3405 if err_msg:
3406 parser.error('Tryserver master cannot be found because: %s\n'
3407 'Please manually specify the tryserver master'
3408 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003409
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003410 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003411 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003412 if not options.bot:
3413 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00003414
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003415 # Get try masters from PRESUBMIT.py files.
3416 masters = presubmit_support.DoGetTryMasters(
3417 change,
3418 change.LocalPaths(),
3419 settings.GetRoot(),
3420 None,
3421 None,
3422 options.verbose,
3423 sys.stdout)
3424 if masters:
3425 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00003426
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003427 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
3428 options.bot = presubmit_support.DoGetTrySlaves(
3429 change,
3430 change.LocalPaths(),
3431 settings.GetRoot(),
3432 None,
3433 None,
3434 options.verbose,
3435 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00003436
3437 if not options.bot:
3438 # Get try masters from cq.cfg if any.
3439 # TODO(tandrii): some (but very few) projects store cq.cfg in different
3440 # location.
3441 cq_cfg = os.path.join(change.RepositoryRoot(),
3442 'infra', 'config', 'cq.cfg')
3443 if os.path.exists(cq_cfg):
3444 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00003445 cq_masters = commit_queue.get_master_builder_map(
3446 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00003447 for master, builders in cq_masters.iteritems():
3448 for builder in builders:
3449 # Skip presubmit builders, because these will fail without LGTM.
3450 if 'presubmit' not in builder.lower():
3451 masters.setdefault(master, {})[builder] = ['defaulttests']
3452 if masters:
3453 return masters
3454
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003455 if not options.bot:
3456 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00003457
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003458 builders_and_tests = {}
3459 # TODO(machenbach): The old style command-line options don't support
3460 # multiple try masters yet.
3461 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
3462 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
3463
3464 for bot in old_style:
3465 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003466 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003467 elif ',' in bot:
3468 parser.error('Specify one bot per --bot flag')
3469 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00003470 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003471
3472 for bot, tests in new_style:
3473 builders_and_tests.setdefault(bot, []).extend(tests)
3474
3475 # Return a master map with one master to be backwards compatible. The
3476 # master name defaults to an empty string, which will cause the master
3477 # not to be set on rietveld (deprecated).
3478 return {options.master: builders_and_tests}
3479
3480 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00003481
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003482 for builders in masters.itervalues():
3483 if any('triggered' in b for b in builders):
3484 print >> sys.stderr, (
3485 'ERROR You are trying to send a job to a triggered bot. This type of'
3486 ' bot requires an\ninitial job from a parent (usually a builder). '
3487 'Instead send your job to the parent.\n'
3488 'Bot list: %s' % builders)
3489 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00003490
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00003491 patchset = cl.GetMostRecentPatchset()
3492 if patchset and patchset != cl.GetPatchset():
3493 print(
3494 '\nWARNING Mismatch between local config and server. Did a previous '
3495 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3496 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003497 if options.luci:
3498 trigger_luci_job(cl, masters, options)
3499 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003500 try:
3501 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
3502 except BuildbucketResponseException as ex:
3503 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00003504 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003505 except Exception as e:
3506 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3507 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
3508 e, stacktrace)
3509 return 1
3510 else:
3511 try:
3512 cl.RpcServer().trigger_distributed_try_jobs(
3513 cl.GetIssue(), patchset, options.name, options.clobber,
3514 options.revision, masters)
3515 except urllib2.HTTPError as e:
3516 if e.code == 404:
3517 print('404 from rietveld; '
3518 'did you mean to use "git try" instead of "git cl try"?')
3519 return 1
3520 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003521
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003522 for (master, builders) in sorted(masters.iteritems()):
3523 if master:
3524 print 'Master: %s' % master
3525 length = max(len(builder) for builder in builders)
3526 for builder in sorted(builders):
3527 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00003528 return 0
3529
3530
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00003531def CMDtry_results(parser, args):
3532 group = optparse.OptionGroup(parser, "Try job results options")
3533 group.add_option(
3534 "-p", "--patchset", type=int, help="patchset number if not current.")
3535 group.add_option(
3536 "--print-master", action='store_true', help="print master name as well")
3537 group.add_option(
3538 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3539 help="Host of buildbucket. The default host is %default.")
3540 parser.add_option_group(group)
3541 auth.add_auth_options(parser)
3542 options, args = parser.parse_args(args)
3543 if args:
3544 parser.error('Unrecognized args: %s' % ' '.join(args))
3545
3546 auth_config = auth.extract_auth_config_from_options(options)
3547 cl = Changelist(auth_config=auth_config)
3548 if not cl.GetIssue():
3549 parser.error('Need to upload first')
3550
3551 if not options.patchset:
3552 options.patchset = cl.GetMostRecentPatchset()
3553 if options.patchset and options.patchset != cl.GetPatchset():
3554 print(
3555 '\nWARNING Mismatch between local config and server. Did a previous '
3556 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3557 'Continuing using\npatchset %s.\n' % options.patchset)
3558 try:
3559 jobs = fetch_try_jobs(auth_config, cl, options)
3560 except BuildbucketResponseException as ex:
3561 print 'Buildbucket error: %s' % ex
3562 return 1
3563 except Exception as e:
3564 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3565 print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
3566 e, stacktrace)
3567 return 1
3568 print_tryjobs(options, jobs)
3569 return 0
3570
3571
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003572@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003573def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003574 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003575 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003576 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003577 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003578
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003579 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003580 if args:
3581 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003582 branch = cl.GetBranch()
3583 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003584 cl = Changelist()
3585 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003586
3587 # Clear configured merge-base, if there is one.
3588 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003589 else:
3590 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003591 return 0
3592
3593
thestig@chromium.org00858c82013-12-02 23:08:03 +00003594def CMDweb(parser, args):
3595 """Opens the current CL in the web browser."""
3596 _, args = parser.parse_args(args)
3597 if args:
3598 parser.error('Unrecognized args: %s' % ' '.join(args))
3599
3600 issue_url = Changelist().GetIssueURL()
3601 if not issue_url:
3602 print >> sys.stderr, 'ERROR No issue to open'
3603 return 1
3604
3605 webbrowser.open(issue_url)
3606 return 0
3607
3608
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003609def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003610 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003611 auth.add_auth_options(parser)
3612 options, args = parser.parse_args(args)
3613 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003614 if args:
3615 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003616 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003617 props = cl.GetIssueProperties()
3618 if props.get('private'):
3619 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003620 cl.SetFlag('commit', '1')
3621 return 0
3622
3623
groby@chromium.org411034a2013-02-26 15:12:01 +00003624def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003625 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003626 auth.add_auth_options(parser)
3627 options, args = parser.parse_args(args)
3628 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00003629 if args:
3630 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003631 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00003632 # Ensure there actually is an issue to close.
3633 cl.GetDescription()
3634 cl.CloseIssue()
3635 return 0
3636
3637
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003638def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003639 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003640 auth.add_auth_options(parser)
3641 options, args = parser.parse_args(args)
3642 auth_config = auth.extract_auth_config_from_options(options)
3643 if args:
3644 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003645
3646 # Uncommitted (staged and unstaged) changes will be destroyed by
3647 # "git reset --hard" if there are merging conflicts in PatchIssue().
3648 # Staged changes would be committed along with the patch from last
3649 # upload, hence counted toward the "last upload" side in the final
3650 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003651 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003652 return 1
3653
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003654 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003655 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003656 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003657 if not issue:
3658 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003659 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003660 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003661
3662 # Create a new branch based on the merge-base
3663 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
3664 try:
3665 # Patch in the latest changes from rietveld.
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003666 rtn = PatchIssue(issue, False, False, None, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003667 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003668 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003669 return rtn
3670
wychen@chromium.org06928532015-02-03 02:11:29 +00003671 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003672 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00003673 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003674 finally:
3675 RunGit(['checkout', '-q', branch])
3676 RunGit(['branch', '-D', TMP_BRANCH])
3677
3678 return 0
3679
3680
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003681def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003682 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003683 parser.add_option(
3684 '--no-color',
3685 action='store_true',
3686 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003687 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003688 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003689 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003690
3691 author = RunGit(['config', 'user.email']).strip() or None
3692
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003693 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003694
3695 if args:
3696 if len(args) > 1:
3697 parser.error('Unknown args')
3698 base_branch = args[0]
3699 else:
3700 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003701 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003702
3703 change = cl.GetChange(base_branch, None)
3704 return owners_finder.OwnersFinder(
3705 [f.LocalPath() for f in
3706 cl.GetChange(base_branch, None).AffectedFiles()],
3707 change.RepositoryRoot(), author,
3708 fopen=file, os_path=os.path, glob=glob.glob,
3709 disable_color=options.no_color).run()
3710
3711
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003712def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003713 """Generates a diff command."""
3714 # Generate diff for the current branch's changes.
3715 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
3716 upstream_commit, '--' ]
3717
3718 if args:
3719 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003720 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003721 diff_cmd.append(arg)
3722 else:
3723 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003724
3725 return diff_cmd
3726
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003727def MatchingFileType(file_name, extensions):
3728 """Returns true if the file name ends with one of the given extensions."""
3729 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003730
enne@chromium.org555cfe42014-01-29 18:21:39 +00003731@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003732def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003733 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00003734 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00003735 GN_EXTS = ['.gn', '.gni']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003736 parser.add_option('--full', action='store_true',
3737 help='Reformat the full content of all touched files')
3738 parser.add_option('--dry-run', action='store_true',
3739 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003740 parser.add_option('--python', action='store_true',
3741 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003742 parser.add_option('--diff', action='store_true',
3743 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003744 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003745
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003746 # git diff generates paths against the root of the repository. Change
3747 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003748 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003749 if rel_base_path:
3750 os.chdir(rel_base_path)
3751
digit@chromium.org29e47272013-05-17 17:01:46 +00003752 # Grab the merge-base commit, i.e. the upstream commit of the current
3753 # branch when it was created or the last time it was rebased. This is
3754 # to cover the case where the user may have called "git fetch origin",
3755 # moving the origin branch to a newer commit, but hasn't rebased yet.
3756 upstream_commit = None
3757 cl = Changelist()
3758 upstream_branch = cl.GetUpstreamBranch()
3759 if upstream_branch:
3760 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
3761 upstream_commit = upstream_commit.strip()
3762
3763 if not upstream_commit:
3764 DieWithError('Could not find base commit for this branch. '
3765 'Are you in detached state?')
3766
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003767 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
3768 diff_output = RunGit(changed_files_cmd)
3769 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00003770 # Filter out files deleted by this CL
3771 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003772
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003773 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
3774 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
3775 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00003776 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00003777
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003778 top_dir = os.path.normpath(
3779 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
3780
3781 # Locate the clang-format binary in the checkout
3782 try:
3783 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
3784 except clang_format.NotFoundError, e:
3785 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00003786
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003787 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
3788 # formatted. This is used to block during the presubmit.
3789 return_value = 0
3790
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003791 if clang_diff_files:
3792 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003793 cmd = [clang_format_tool]
3794 if not opts.dry_run and not opts.diff:
3795 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003796 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003797 if opts.diff:
3798 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003799 else:
3800 env = os.environ.copy()
3801 env['PATH'] = str(os.path.dirname(clang_format_tool))
3802 try:
3803 script = clang_format.FindClangFormatScriptInChromiumTree(
3804 'clang-format-diff.py')
3805 except clang_format.NotFoundError, e:
3806 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003807
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003808 cmd = [sys.executable, script, '-p0']
3809 if not opts.dry_run and not opts.diff:
3810 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003811
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003812 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
3813 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003814
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003815 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
3816 if opts.diff:
3817 sys.stdout.write(stdout)
3818 if opts.dry_run and len(stdout) > 0:
3819 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003820
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003821 # Similar code to above, but using yapf on .py files rather than clang-format
3822 # on C/C++ files
3823 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003824 yapf_tool = gclient_utils.FindExecutable('yapf')
3825 if yapf_tool is None:
3826 DieWithError('yapf not found in PATH')
3827
3828 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003829 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003830 cmd = [yapf_tool]
3831 if not opts.dry_run and not opts.diff:
3832 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003833 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003834 if opts.diff:
3835 sys.stdout.write(stdout)
3836 else:
3837 # TODO(sbc): yapf --lines mode still has some issues.
3838 # https://github.com/google/yapf/issues/154
3839 DieWithError('--python currently only works with --full')
3840
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003841 # Dart's formatter does not have the nice property of only operating on
3842 # modified chunks, so hard code full.
3843 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003844 try:
3845 command = [dart_format.FindDartFmtToolInChromiumTree()]
3846 if not opts.dry_run and not opts.diff:
3847 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003848 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003849
ppi@chromium.org6593d932016-03-03 15:41:15 +00003850 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003851 if opts.dry_run and stdout:
3852 return_value = 2
3853 except dart_format.NotFoundError as e:
erikcorry@chromium.org3e445022015-12-17 09:07:26 +00003854 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
3855 'found in this checkout. Files in other languages are still ' +
3856 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003857
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00003858 # Format GN build files. Always run on full build files for canonical form.
3859 if gn_diff_files:
3860 cmd = ['gn', 'format']
3861 if not opts.dry_run and not opts.diff:
3862 cmd.append('--in-place')
3863 for gn_diff_file in gn_diff_files:
3864 stdout = RunCommand(cmd + [gn_diff_file], cwd=top_dir)
3865 if opts.diff:
3866 sys.stdout.write(stdout)
3867
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003868 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003869
3870
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00003871@subcommand.usage('<codereview url or issue id>')
3872def CMDcheckout(parser, args):
3873 """Checks out a branch associated with a given Rietveld issue."""
3874 _, args = parser.parse_args(args)
3875
3876 if len(args) != 1:
3877 parser.print_help()
3878 return 1
3879
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003880 target_issue = ParseIssueNum(args[0])
3881 if target_issue == None:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00003882 parser.print_help()
3883 return 1
3884
3885 key_and_issues = [x.split() for x in RunGit(
3886 ['config', '--local', '--get-regexp', r'branch\..*\.rietveldissue'])
3887 .splitlines()]
3888 branches = []
3889 for key, issue in key_and_issues:
3890 if issue == target_issue:
3891 branches.append(re.sub(r'branch\.(.*)\.rietveldissue', r'\1', key))
3892
3893 if len(branches) == 0:
3894 print 'No branch found for issue %s.' % target_issue
3895 return 1
3896 if len(branches) == 1:
3897 RunGit(['checkout', branches[0]])
3898 else:
3899 print 'Multiple branches match issue %s:' % target_issue
3900 for i in range(len(branches)):
3901 print '%d: %s' % (i, branches[i])
3902 which = raw_input('Choose by index: ')
3903 try:
3904 RunGit(['checkout', branches[int(which)]])
3905 except (IndexError, ValueError):
3906 print 'Invalid selection, not checking out any branch.'
3907 return 1
3908
3909 return 0
3910
3911
maruel@chromium.org29404b52014-09-08 22:58:00 +00003912def CMDlol(parser, args):
3913 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00003914 print zlib.decompress(base64.b64decode(
3915 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
3916 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
3917 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
3918 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00003919 return 0
3920
3921
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003922class OptionParser(optparse.OptionParser):
3923 """Creates the option parse and add --verbose support."""
3924 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003925 optparse.OptionParser.__init__(
3926 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003927 self.add_option(
3928 '-v', '--verbose', action='count', default=0,
3929 help='Use 2 times for more debugging info')
3930
3931 def parse_args(self, args=None, values=None):
3932 options, args = optparse.OptionParser.parse_args(self, args, values)
3933 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
3934 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
3935 return options, args
3936
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003937
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003938def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00003939 if sys.hexversion < 0x02060000:
3940 print >> sys.stderr, (
3941 '\nYour python version %s is unsupported, please upgrade.\n' %
3942 sys.version.split(' ', 1)[0])
3943 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003944
maruel@chromium.orgddd59412011-11-30 14:20:38 +00003945 # Reload settings.
3946 global settings
3947 settings = Settings()
3948
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003949 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003950 dispatcher = subcommand.CommandDispatcher(__name__)
3951 try:
3952 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003953 except auth.AuthenticationError as e:
3954 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003955 except urllib2.HTTPError, e:
3956 if e.code != 500:
3957 raise
3958 DieWithError(
3959 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
3960 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00003961 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003962
3963
3964if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003965 # These affect sys.stdout so do it outside of main() to simplify mocks in
3966 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00003967 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003968 colorama.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00003969 try:
3970 sys.exit(main(sys.argv[1:]))
3971 except KeyboardInterrupt:
3972 sys.stderr.write('interrupted\n')
3973 sys.exit(1)