blob: 7e9e4ae1373ba76220b9d77b8565d73513e51a11 [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'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000255 error = content_json.get('error')
256 if error.get('code') == 403:
257 raise BuildbucketResponseException(
258 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000259 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000260 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000261 raise BuildbucketResponseException(msg)
262
263 if response.status == 200:
264 if not content_json:
265 raise BuildbucketResponseException(
266 'Buildbucket returns invalid json content: %s.\n'
267 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
268 content)
269 return content_json
270 if response.status < 500 or try_count >= 2:
271 raise httplib2.HttpLib2Error(content)
272
273 # status >= 500 means transient failures.
274 logging.debug('Transient errors when %s. Will retry.', operation_name)
275 time.sleep(0.5 + 1.5*try_count)
276 try_count += 1
277 assert False, 'unreachable'
278
279
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000280def trigger_luci_job(changelist, masters, options):
281 """Send a job to run on LUCI."""
282 issue_props = changelist.GetIssueProperties()
283 issue = changelist.GetIssue()
284 patchset = changelist.GetMostRecentPatchset()
285 for builders_and_tests in sorted(masters.itervalues()):
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000286 # TODO(hinoka et al): add support for other properties.
287 # Currently, this completely ignores testfilter and other properties.
288 for builder in sorted(builders_and_tests):
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000289 luci_trigger.trigger(
290 builder, 'HEAD', issue, patchset, issue_props['project'])
291
292
machenbach@chromium.org45453142015-09-15 08:45:22 +0000293def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000294 rietveld_url = settings.GetDefaultServerUrl()
295 rietveld_host = urlparse.urlparse(rietveld_url).hostname
296 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
297 http = authenticator.authorize(httplib2.Http())
298 http.force_exception_to_status_code = True
299 issue_props = changelist.GetIssueProperties()
300 issue = changelist.GetIssue()
301 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000302 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000303
304 buildbucket_put_url = (
305 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000306 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000307 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
308 hostname=rietveld_host,
309 issue=issue,
310 patch=patchset)
311
312 batch_req_body = {'builds': []}
313 print_text = []
314 print_text.append('Tried jobs on:')
315 for master, builders_and_tests in sorted(masters.iteritems()):
316 print_text.append('Master: %s' % master)
317 bucket = _prefix_master(master)
318 for builder, tests in sorted(builders_and_tests.iteritems()):
319 print_text.append(' %s: %s' % (builder, tests))
320 parameters = {
321 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000322 'changes': [{
323 'author': {'email': issue_props['owner_email']},
324 'revision': options.revision,
325 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000326 'properties': {
327 'category': category,
328 'issue': issue,
329 'master': master,
330 'patch_project': issue_props['project'],
331 'patch_storage': 'rietveld',
332 'patchset': patchset,
333 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000334 'rietveld': rietveld_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000335 },
336 }
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000337 if tests:
338 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000339 if properties:
340 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000341 if options.clobber:
342 parameters['properties']['clobber'] = True
343 batch_req_body['builds'].append(
344 {
345 'bucket': bucket,
346 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000347 'client_operation_id': str(uuid.uuid4()),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000348 'tags': ['builder:%s' % builder,
349 'buildset:%s' % buildset,
350 'master:%s' % master,
351 'user_agent:git_cl_try']
352 }
353 )
354
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000355 _buildbucket_retry(
356 'triggering tryjobs',
357 http,
358 buildbucket_put_url,
359 'PUT',
360 body=json.dumps(batch_req_body),
361 headers={'Content-Type': 'application/json'}
362 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000363 print_text.append('To see results here, run: git cl try-results')
364 print_text.append('To see results in browser, run: git cl web')
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000365 print '\n'.join(print_text)
kjellander@chromium.org44424542015-06-02 18:35:29 +0000366
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000367
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000368def fetch_try_jobs(auth_config, changelist, options):
369 """Fetches tryjobs from buildbucket.
370
371 Returns a map from build id to build info as json dictionary.
372 """
373 rietveld_url = settings.GetDefaultServerUrl()
374 rietveld_host = urlparse.urlparse(rietveld_url).hostname
375 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
376 if authenticator.has_cached_credentials():
377 http = authenticator.authorize(httplib2.Http())
378 else:
379 print ('Warning: Some results might be missing because %s' %
380 # Get the message on how to login.
381 auth.LoginRequiredError(rietveld_host).message)
382 http = httplib2.Http()
383
384 http.force_exception_to_status_code = True
385
386 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
387 hostname=rietveld_host,
388 issue=changelist.GetIssue(),
389 patch=options.patchset)
390 params = {'tag': 'buildset:%s' % buildset}
391
392 builds = {}
393 while True:
394 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
395 hostname=options.buildbucket_host,
396 params=urllib.urlencode(params))
397 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
398 for build in content.get('builds', []):
399 builds[build['id']] = build
400 if 'next_cursor' in content:
401 params['start_cursor'] = content['next_cursor']
402 else:
403 break
404 return builds
405
406
407def print_tryjobs(options, builds):
408 """Prints nicely result of fetch_try_jobs."""
409 if not builds:
410 print 'No tryjobs scheduled'
411 return
412
413 # Make a copy, because we'll be modifying builds dictionary.
414 builds = builds.copy()
415 builder_names_cache = {}
416
417 def get_builder(b):
418 try:
419 return builder_names_cache[b['id']]
420 except KeyError:
421 try:
422 parameters = json.loads(b['parameters_json'])
423 name = parameters['builder_name']
424 except (ValueError, KeyError) as error:
425 print 'WARNING: failed to get builder name for build %s: %s' % (
426 b['id'], error)
427 name = None
428 builder_names_cache[b['id']] = name
429 return name
430
431 def get_bucket(b):
432 bucket = b['bucket']
433 if bucket.startswith('master.'):
434 return bucket[len('master.'):]
435 return bucket
436
437 if options.print_master:
438 name_fmt = '%%-%ds %%-%ds' % (
439 max(len(str(get_bucket(b))) for b in builds.itervalues()),
440 max(len(str(get_builder(b))) for b in builds.itervalues()))
441 def get_name(b):
442 return name_fmt % (get_bucket(b), get_builder(b))
443 else:
444 name_fmt = '%%-%ds' % (
445 max(len(str(get_builder(b))) for b in builds.itervalues()))
446 def get_name(b):
447 return name_fmt % get_builder(b)
448
449 def sort_key(b):
450 return b['status'], b.get('result'), get_name(b), b.get('url')
451
452 def pop(title, f, color=None, **kwargs):
453 """Pop matching builds from `builds` dict and print them."""
454
455 if not sys.stdout.isatty() or color is None:
456 colorize = str
457 else:
458 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
459
460 result = []
461 for b in builds.values():
462 if all(b.get(k) == v for k, v in kwargs.iteritems()):
463 builds.pop(b['id'])
464 result.append(b)
465 if result:
466 print colorize(title)
467 for b in sorted(result, key=sort_key):
468 print ' ', colorize('\t'.join(map(str, f(b))))
469
470 total = len(builds)
471 pop(status='COMPLETED', result='SUCCESS',
472 title='Successes:', color=Fore.GREEN,
473 f=lambda b: (get_name(b), b.get('url')))
474 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
475 title='Infra Failures:', color=Fore.MAGENTA,
476 f=lambda b: (get_name(b), b.get('url')))
477 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
478 title='Failures:', color=Fore.RED,
479 f=lambda b: (get_name(b), b.get('url')))
480 pop(status='COMPLETED', result='CANCELED',
481 title='Canceled:', color=Fore.MAGENTA,
482 f=lambda b: (get_name(b),))
483 pop(status='COMPLETED', result='FAILURE',
484 failure_reason='INVALID_BUILD_DEFINITION',
485 title='Wrong master/builder name:', color=Fore.MAGENTA,
486 f=lambda b: (get_name(b),))
487 pop(status='COMPLETED', result='FAILURE',
488 title='Other failures:',
489 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
490 pop(status='COMPLETED',
491 title='Other finished:',
492 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
493 pop(status='STARTED',
494 title='Started:', color=Fore.YELLOW,
495 f=lambda b: (get_name(b), b.get('url')))
496 pop(status='SCHEDULED',
497 title='Scheduled:',
498 f=lambda b: (get_name(b), 'id=%s' % b['id']))
499 # The last section is just in case buildbucket API changes OR there is a bug.
500 pop(title='Other:',
501 f=lambda b: (get_name(b), 'id=%s' % b['id']))
502 assert len(builds) == 0
503 print 'Total: %d tryjobs' % total
504
505
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000506def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
507 """Return the corresponding git ref if |base_url| together with |glob_spec|
508 matches the full |url|.
509
510 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
511 """
512 fetch_suburl, as_ref = glob_spec.split(':')
513 if allow_wildcards:
514 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
515 if glob_match:
516 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
517 # "branches/{472,597,648}/src:refs/remotes/svn/*".
518 branch_re = re.escape(base_url)
519 if glob_match.group(1):
520 branch_re += '/' + re.escape(glob_match.group(1))
521 wildcard = glob_match.group(2)
522 if wildcard == '*':
523 branch_re += '([^/]*)'
524 else:
525 # Escape and replace surrounding braces with parentheses and commas
526 # with pipe symbols.
527 wildcard = re.escape(wildcard)
528 wildcard = re.sub('^\\\\{', '(', wildcard)
529 wildcard = re.sub('\\\\,', '|', wildcard)
530 wildcard = re.sub('\\\\}$', ')', wildcard)
531 branch_re += wildcard
532 if glob_match.group(3):
533 branch_re += re.escape(glob_match.group(3))
534 match = re.match(branch_re, url)
535 if match:
536 return re.sub('\*$', match.group(1), as_ref)
537
538 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
539 if fetch_suburl:
540 full_url = base_url + '/' + fetch_suburl
541 else:
542 full_url = base_url
543 if full_url == url:
544 return as_ref
545 return None
546
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000547
iannucci@chromium.org79540052012-10-19 23:15:26 +0000548def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000549 """Prints statistics about the change to the user."""
550 # --no-ext-diff is broken in some versions of Git, so try to work around
551 # this by overriding the environment (but there is still a problem if the
552 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000553 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000554 if 'GIT_EXTERNAL_DIFF' in env:
555 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000556
557 if find_copies:
558 similarity_options = ['--find-copies-harder', '-l100000',
559 '-C%s' % similarity]
560 else:
561 similarity_options = ['-M%s' % similarity]
562
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000563 try:
564 stdout = sys.stdout.fileno()
565 except AttributeError:
566 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000567 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000568 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000569 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000570 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000571
572
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000573class BuildbucketResponseException(Exception):
574 pass
575
576
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000577class Settings(object):
578 def __init__(self):
579 self.default_server = None
580 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000581 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000582 self.is_git_svn = None
583 self.svn_branch = None
584 self.tree_status_url = None
585 self.viewvc_url = None
586 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000587 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000588 self.squash_gerrit_uploads = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000589 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000590 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000591 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000592 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000593
594 def LazyUpdateIfNeeded(self):
595 """Updates the settings from a codereview.settings file, if available."""
596 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000597 # The only value that actually changes the behavior is
598 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000599 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000600 error_ok=True
601 ).strip().lower()
602
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000603 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000604 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000605 LoadCodereviewSettingsFromFile(cr_settings_file)
606 self.updated = True
607
608 def GetDefaultServerUrl(self, error_ok=False):
609 if not self.default_server:
610 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000611 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000612 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000613 if error_ok:
614 return self.default_server
615 if not self.default_server:
616 error_message = ('Could not find settings file. You must configure '
617 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000618 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000619 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000620 return self.default_server
621
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000622 @staticmethod
623 def GetRelativeRoot():
624 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000625
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000626 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000627 if self.root is None:
628 self.root = os.path.abspath(self.GetRelativeRoot())
629 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000630
631 def GetIsGitSvn(self):
632 """Return true if this repo looks like it's using git-svn."""
633 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000634 if self.GetPendingRefPrefix():
635 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
636 self.is_git_svn = False
637 else:
638 # If you have any "svn-remote.*" config keys, we think you're using svn.
639 self.is_git_svn = RunGitWithCode(
640 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000641 return self.is_git_svn
642
643 def GetSVNBranch(self):
644 if self.svn_branch is None:
645 if not self.GetIsGitSvn():
646 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
647
648 # Try to figure out which remote branch we're based on.
649 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000650 # 1) iterate through our branch history and find the svn URL.
651 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000652
653 # regexp matching the git-svn line that contains the URL.
654 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
655
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000656 # We don't want to go through all of history, so read a line from the
657 # pipe at a time.
658 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000659 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000660 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
661 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000662 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000663 for line in proc.stdout:
664 match = git_svn_re.match(line)
665 if match:
666 url = match.group(1)
667 proc.stdout.close() # Cut pipe.
668 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000669
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000670 if url:
671 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
672 remotes = RunGit(['config', '--get-regexp',
673 r'^svn-remote\..*\.url']).splitlines()
674 for remote in remotes:
675 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000676 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000677 remote = match.group(1)
678 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000679 rewrite_root = RunGit(
680 ['config', 'svn-remote.%s.rewriteRoot' % remote],
681 error_ok=True).strip()
682 if rewrite_root:
683 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000684 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000685 ['config', 'svn-remote.%s.fetch' % remote],
686 error_ok=True).strip()
687 if fetch_spec:
688 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
689 if self.svn_branch:
690 break
691 branch_spec = RunGit(
692 ['config', 'svn-remote.%s.branches' % remote],
693 error_ok=True).strip()
694 if branch_spec:
695 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
696 if self.svn_branch:
697 break
698 tag_spec = RunGit(
699 ['config', 'svn-remote.%s.tags' % remote],
700 error_ok=True).strip()
701 if tag_spec:
702 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
703 if self.svn_branch:
704 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000705
706 if not self.svn_branch:
707 DieWithError('Can\'t guess svn branch -- try specifying it on the '
708 'command line')
709
710 return self.svn_branch
711
712 def GetTreeStatusUrl(self, error_ok=False):
713 if not self.tree_status_url:
714 error_message = ('You must configure your tree status URL by running '
715 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000716 self.tree_status_url = self._GetRietveldConfig(
717 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000718 return self.tree_status_url
719
720 def GetViewVCUrl(self):
721 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000722 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000723 return self.viewvc_url
724
rmistry@google.com90752582014-01-14 21:04:50 +0000725 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000726 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000727
rmistry@google.com78948ed2015-07-08 23:09:57 +0000728 def GetIsSkipDependencyUpload(self, branch_name):
729 """Returns true if specified branch should skip dep uploads."""
730 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
731 error_ok=True)
732
rmistry@google.com5626a922015-02-26 14:03:30 +0000733 def GetRunPostUploadHook(self):
734 run_post_upload_hook = self._GetRietveldConfig(
735 'run-post-upload-hook', error_ok=True)
736 return run_post_upload_hook == "True"
737
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000738 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000739 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000740
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000741 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000742 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000743
ukai@chromium.orge8077812012-02-03 03:41:46 +0000744 def GetIsGerrit(self):
745 """Return true if this repo is assosiated with gerrit code review system."""
746 if self.is_gerrit is None:
747 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
748 return self.is_gerrit
749
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000750 def GetSquashGerritUploads(self):
751 """Return true if uploads to Gerrit should be squashed by default."""
752 if self.squash_gerrit_uploads is None:
753 self.squash_gerrit_uploads = (
754 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
755 error_ok=True).strip() == 'true')
756 return self.squash_gerrit_uploads
757
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000758 def GetGitEditor(self):
759 """Return the editor specified in the git config, or None if none is."""
760 if self.git_editor is None:
761 self.git_editor = self._GetConfig('core.editor', error_ok=True)
762 return self.git_editor or None
763
thestig@chromium.org44202a22014-03-11 19:22:18 +0000764 def GetLintRegex(self):
765 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
766 DEFAULT_LINT_REGEX)
767
768 def GetLintIgnoreRegex(self):
769 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
770 DEFAULT_LINT_IGNORE_REGEX)
771
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000772 def GetProject(self):
773 if not self.project:
774 self.project = self._GetRietveldConfig('project', error_ok=True)
775 return self.project
776
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000777 def GetForceHttpsCommitUrl(self):
778 if not self.force_https_commit_url:
779 self.force_https_commit_url = self._GetRietveldConfig(
780 'force-https-commit-url', error_ok=True)
781 return self.force_https_commit_url
782
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000783 def GetPendingRefPrefix(self):
784 if not self.pending_ref_prefix:
785 self.pending_ref_prefix = self._GetRietveldConfig(
786 'pending-ref-prefix', error_ok=True)
787 return self.pending_ref_prefix
788
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000789 def _GetRietveldConfig(self, param, **kwargs):
790 return self._GetConfig('rietveld.' + param, **kwargs)
791
rmistry@google.com78948ed2015-07-08 23:09:57 +0000792 def _GetBranchConfig(self, branch_name, param, **kwargs):
793 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
794
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000795 def _GetConfig(self, param, **kwargs):
796 self.LazyUpdateIfNeeded()
797 return RunGit(['config', param], **kwargs).strip()
798
799
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000800def ShortBranchName(branch):
801 """Convert a name like 'refs/heads/foo' to just 'foo'."""
802 return branch.replace('refs/heads/', '')
803
804
805class Changelist(object):
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000806 def __init__(self, branchref=None, issue=None, auth_config=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000807 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000808 global settings
809 if not settings:
810 # Happens when git_cl.py is used as a utility library.
811 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000812 settings.GetDefaultServerUrl()
813 self.branchref = branchref
814 if self.branchref:
815 self.branch = ShortBranchName(self.branchref)
816 else:
817 self.branch = None
818 self.rietveld_server = None
819 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000820 self.lookedup_issue = False
821 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000822 self.has_description = False
823 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000824 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000825 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000826 self.cc = None
827 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000828 self._auth_config = auth_config
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000829 self._props = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000830 self._remote = None
831 self._rpc_server = None
832
833 @property
834 def auth_config(self):
835 return self._auth_config
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000836
837 def GetCCList(self):
838 """Return the users cc'd on this CL.
839
840 Return is a string suitable for passing to gcl with the --cc flag.
841 """
842 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000843 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000844 more_cc = ','.join(self.watchers)
845 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
846 return self.cc
847
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000848 def GetCCListWithoutDefault(self):
849 """Return the users cc'd on this CL excluding default ones."""
850 if self.cc is None:
851 self.cc = ','.join(self.watchers)
852 return self.cc
853
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000854 def SetWatchers(self, watchers):
855 """Set the list of email addresses that should be cc'd based on the changed
856 files in this CL.
857 """
858 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000859
860 def GetBranch(self):
861 """Returns the short branch name, e.g. 'master'."""
862 if not self.branch:
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000863 branchref = RunGit(['symbolic-ref', 'HEAD'],
864 stderr=subprocess2.VOID, error_ok=True).strip()
865 if not branchref:
866 return None
867 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000868 self.branch = ShortBranchName(self.branchref)
869 return self.branch
870
871 def GetBranchRef(self):
872 """Returns the full branch name, e.g. 'refs/heads/master'."""
873 self.GetBranch() # Poke the lazy loader.
874 return self.branchref
875
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000876 @staticmethod
877 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000878 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000879 e.g. 'origin', 'refs/heads/master'
880 """
881 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000882 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
883 error_ok=True).strip()
884 if upstream_branch:
885 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
886 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000887 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
888 error_ok=True).strip()
889 if upstream_branch:
890 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000891 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000892 # Fall back on trying a git-svn upstream branch.
893 if settings.GetIsGitSvn():
894 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000895 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000896 # Else, try to guess the origin remote.
897 remote_branches = RunGit(['branch', '-r']).split()
898 if 'origin/master' in remote_branches:
899 # Fall back on origin/master if it exits.
900 remote = 'origin'
901 upstream_branch = 'refs/heads/master'
902 elif 'origin/trunk' in remote_branches:
903 # Fall back on origin/trunk if it exists. Generally a shared
904 # git-svn clone
905 remote = 'origin'
906 upstream_branch = 'refs/heads/trunk'
907 else:
908 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000909Either pass complete "git diff"-style arguments, like
910 git cl upload origin/master
911or verify this branch is set up to track another (via the --track argument to
912"git checkout -b ...").""")
913
914 return remote, upstream_branch
915
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000916 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000917 upstream_branch = self.GetUpstreamBranch()
918 if not BranchExists(upstream_branch):
919 DieWithError('The upstream for the current branch (%s) does not exist '
920 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +0000921 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000922 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000923
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000924 def GetUpstreamBranch(self):
925 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000926 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000927 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000928 upstream_branch = upstream_branch.replace('refs/heads/',
929 'refs/remotes/%s/' % remote)
930 upstream_branch = upstream_branch.replace('refs/branch-heads/',
931 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000932 self.upstream_branch = upstream_branch
933 return self.upstream_branch
934
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000935 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000936 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000937 remote, branch = None, self.GetBranch()
938 seen_branches = set()
939 while branch not in seen_branches:
940 seen_branches.add(branch)
941 remote, branch = self.FetchUpstreamTuple(branch)
942 branch = ShortBranchName(branch)
943 if remote != '.' or branch.startswith('refs/remotes'):
944 break
945 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000946 remotes = RunGit(['remote'], error_ok=True).split()
947 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000948 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000949 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000950 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000951 logging.warning('Could not determine which remote this change is '
952 'associated with, so defaulting to "%s". This may '
953 'not be what you want. You may prevent this message '
954 'by running "git svn info" as documented here: %s',
955 self._remote,
956 GIT_INSTRUCTIONS_URL)
957 else:
958 logging.warn('Could not determine which remote this change is '
959 'associated with. You may prevent this message by '
960 'running "git svn info" as documented here: %s',
961 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000962 branch = 'HEAD'
963 if branch.startswith('refs/remotes'):
964 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000965 elif branch.startswith('refs/branch-heads/'):
966 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000967 else:
968 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000969 return self._remote
970
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000971 def GitSanityChecks(self, upstream_git_obj):
972 """Checks git repo status and ensures diff is from local commits."""
973
sbc@chromium.org79706062015-01-14 21:18:12 +0000974 if upstream_git_obj is None:
975 if self.GetBranch() is None:
976 print >> sys.stderr, (
dpranke@chromium.orgee87f582015-07-31 18:46:25 +0000977 'ERROR: unable to determine current branch (detached HEAD?)')
sbc@chromium.org79706062015-01-14 21:18:12 +0000978 else:
979 print >> sys.stderr, (
980 'ERROR: no upstream branch')
981 return False
982
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000983 # Verify the commit we're diffing against is in our current branch.
984 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
985 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
986 if upstream_sha != common_ancestor:
987 print >> sys.stderr, (
988 'ERROR: %s is not in the current branch. You may need to rebase '
989 'your tracking branch' % upstream_sha)
990 return False
991
992 # List the commits inside the diff, and verify they are all local.
993 commits_in_diff = RunGit(
994 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
995 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
996 remote_branch = remote_branch.strip()
997 if code != 0:
998 _, remote_branch = self.GetRemoteBranch()
999
1000 commits_in_remote = RunGit(
1001 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1002
1003 common_commits = set(commits_in_diff) & set(commits_in_remote)
1004 if common_commits:
1005 print >> sys.stderr, (
1006 'ERROR: Your diff contains %d commits already in %s.\n'
1007 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1008 'the diff. If you are using a custom git flow, you can override'
1009 ' the reference used for this check with "git config '
1010 'gitcl.remotebranch <git-ref>".' % (
1011 len(common_commits), remote_branch, upstream_git_obj))
1012 return False
1013 return True
1014
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001015 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001016 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001017
1018 Returns None if it is not set.
1019 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001020 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
1021 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001022
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001023 def GetGitSvnRemoteUrl(self):
1024 """Return the configured git-svn remote URL parsed from git svn info.
1025
1026 Returns None if it is not set.
1027 """
1028 # URL is dependent on the current directory.
1029 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1030 if data:
1031 keys = dict(line.split(': ', 1) for line in data.splitlines()
1032 if ': ' in line)
1033 return keys.get('URL', None)
1034 return None
1035
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001036 def GetRemoteUrl(self):
1037 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1038
1039 Returns None if there is no remote.
1040 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001041 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001042 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1043
1044 # If URL is pointing to a local directory, it is probably a git cache.
1045 if os.path.isdir(url):
1046 url = RunGit(['config', 'remote.%s.url' % remote],
1047 error_ok=True,
1048 cwd=url).strip()
1049 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001050
1051 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001052 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001053 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001054 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001055 self.issue = int(issue) or None if issue else None
1056 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001057 return self.issue
1058
1059 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +00001060 if not self.rietveld_server:
1061 # If we're on a branch then get the server potentially associated
1062 # with that branch.
1063 if self.GetIssue():
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001064 rietveld_server_config = self._RietveldServer()
1065 if rietveld_server_config:
1066 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1067 ['config', rietveld_server_config], error_ok=True).strip())
evan@chromium.org0af9b702012-02-11 00:42:16 +00001068 if not self.rietveld_server:
1069 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001070 return self.rietveld_server
1071
1072 def GetIssueURL(self):
1073 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001074 if not self.GetIssue():
1075 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001076 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
1077
1078 def GetDescription(self, pretty=False):
1079 if not self.has_description:
1080 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +00001081 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +00001082 try:
1083 self.description = self.RpcServer().get_description(issue).strip()
maruel@chromium.org85616e02014-07-28 15:37:55 +00001084 except urllib2.HTTPError as e:
miket@chromium.org183df1a2012-01-04 19:44:55 +00001085 if e.code == 404:
1086 DieWithError(
1087 ('\nWhile fetching the description for issue %d, received a '
1088 '404 (not found)\n'
1089 'error. It is likely that you deleted this '
1090 'issue on the server. If this is the\n'
1091 'case, please run\n\n'
1092 ' git cl issue 0\n\n'
1093 'to clear the association with the deleted issue. Then run '
1094 'this command again.') % issue)
1095 else:
1096 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +00001097 '\nFailed to fetch issue description. HTTP error %d' % e.code)
maruel@chromium.org85616e02014-07-28 15:37:55 +00001098 except urllib2.URLError as e:
1099 print >> sys.stderr, (
1100 'Warning: Failed to retrieve CL description due to network '
1101 'failure.')
1102 self.description = ''
1103
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001104 self.has_description = True
1105 if pretty:
1106 wrapper = textwrap.TextWrapper()
1107 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1108 return wrapper.fill(self.description)
1109 return self.description
1110
1111 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001112 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001113 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001114 patchset = RunGit(['config', self._PatchsetSetting()],
1115 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001116 self.patchset = int(patchset) or None if patchset else None
1117 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001118 return self.patchset
1119
1120 def SetPatchset(self, patchset):
1121 """Set this branch's patchset. If patchset=0, clears the patchset."""
1122 if patchset:
1123 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001124 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001125 else:
1126 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001127 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001128 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001129
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001130 def GetMostRecentPatchset(self):
1131 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +00001132
1133 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001134 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001135 '/download/issue%s_%s.diff' % (issue, patchset))
1136
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001137 def GetIssueProperties(self):
1138 if self._props is None:
1139 issue = self.GetIssue()
1140 if not issue:
1141 self._props = {}
1142 else:
1143 self._props = self.RpcServer().get_issue_properties(issue, True)
1144 return self._props
1145
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001146 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001147 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001148
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001149 def AddComment(self, message):
1150 return self.RpcServer().add_comment(self.GetIssue(), message)
1151
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001152 def SetIssue(self, issue):
1153 """Set this branch's issue. If issue=0, clears the issue."""
1154 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001155 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001156 RunGit(['config', self._IssueSetting(), str(issue)])
1157 if self.rietveld_server:
1158 RunGit(['config', self._RietveldServer(), self.rietveld_server])
1159 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +00001160 current_issue = self.GetIssue()
1161 if current_issue:
1162 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001163 self.issue = None
1164 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001165
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001166 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001167 if not self.GitSanityChecks(upstream_branch):
1168 DieWithError('\nGit sanity check failure')
1169
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001170 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001171 if not root:
1172 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001173 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001174
1175 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001176 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001177 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001178 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001179 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001180 except subprocess2.CalledProcessError:
1181 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001182 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001183 'This branch probably doesn\'t exist anymore. To reset the\n'
1184 'tracking branch, please run\n'
1185 ' git branch --set-upstream %s trunk\n'
1186 'replacing trunk with origin/master or the relevant branch') %
1187 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001188
maruel@chromium.org52424302012-08-29 15:14:30 +00001189 issue = self.GetIssue()
1190 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001191 if issue:
1192 description = self.GetDescription()
1193 else:
1194 # If the change was never uploaded, use the log messages of all commits
1195 # up to the branch point, as git cl upload will prefill the description
1196 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001197 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1198 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001199
1200 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001201 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001202 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001203 name,
1204 description,
1205 absroot,
1206 files,
1207 issue,
1208 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001209 author,
1210 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001211
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001212 def GetStatus(self):
1213 """Apply a rough heuristic to give a simple summary of an issue's review
1214 or CQ status, assuming adherence to a common workflow.
1215
1216 Returns None if no issue for this branch, or one of the following keywords:
1217 * 'error' - error from review tool (including deleted issues)
1218 * 'unsent' - not sent for review
1219 * 'waiting' - waiting for review
1220 * 'reply' - waiting for owner to reply to review
1221 * 'lgtm' - LGTM from at least one approved reviewer
1222 * 'commit' - in the commit queue
1223 * 'closed' - closed
1224 """
1225 if not self.GetIssue():
1226 return None
1227
1228 try:
1229 props = self.GetIssueProperties()
1230 except urllib2.HTTPError:
1231 return 'error'
1232
1233 if props.get('closed'):
1234 # Issue is closed.
1235 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001236 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001237 # Issue is in the commit queue.
1238 return 'commit'
1239
1240 try:
1241 reviewers = self.GetApprovingReviewers()
1242 except urllib2.HTTPError:
1243 return 'error'
1244
1245 if reviewers:
1246 # Was LGTM'ed.
1247 return 'lgtm'
1248
1249 messages = props.get('messages') or []
1250
1251 if not messages:
1252 # No message was sent.
1253 return 'unsent'
1254 if messages[-1]['sender'] != props.get('owner_email'):
1255 # Non-LGTM reply from non-owner
1256 return 'reply'
1257 return 'waiting'
1258
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001259 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001260 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001261
1262 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001263 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001264 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001265 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001266 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001267 except presubmit_support.PresubmitFailure, e:
1268 DieWithError(
1269 ('%s\nMaybe your depot_tools is out of date?\n'
1270 'If all fails, contact maruel@') % e)
1271
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001272 def UpdateDescription(self, description):
1273 self.description = description
1274 return self.RpcServer().update_description(
1275 self.GetIssue(), self.description)
1276
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001277 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +00001278 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001279 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001280
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001281 def SetFlag(self, flag, value):
1282 """Patchset must match."""
1283 if not self.GetPatchset():
1284 DieWithError('The patchset needs to match. Send another patchset.')
1285 try:
1286 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001287 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001288 except urllib2.HTTPError, e:
1289 if e.code == 404:
1290 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1291 if e.code == 403:
1292 DieWithError(
1293 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1294 'match?') % (self.GetIssue(), self.GetPatchset()))
1295 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001296
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001297 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001298 """Returns an upload.RpcServer() to access this review's rietveld instance.
1299 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001300 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001301 self._rpc_server = rietveld.CachingRietveld(
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001302 self.GetRietveldServer(),
1303 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001304 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001305
1306 def _IssueSetting(self):
1307 """Return the git setting that stores this change's issue."""
1308 return 'branch.%s.rietveldissue' % self.GetBranch()
1309
1310 def _PatchsetSetting(self):
1311 """Return the git setting that stores this change's most recent patchset."""
1312 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1313
1314 def _RietveldServer(self):
1315 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001316 branch = self.GetBranch()
1317 if branch:
1318 return 'branch.%s.rietveldserver' % branch
1319 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001320
1321
1322def GetCodereviewSettingsInteractively():
1323 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001324 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001325 server = settings.GetDefaultServerUrl(error_ok=True)
1326 prompt = 'Rietveld server (host[:port])'
1327 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +00001328 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001329 if not server and not newserver:
1330 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001331 if newserver:
1332 newserver = gclient_utils.UpgradeToHttps(newserver)
1333 if newserver != server:
1334 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001335
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001336 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001337 prompt = caption
1338 if initial:
1339 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +00001340 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001341 if new_val == 'x':
1342 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001343 elif new_val:
1344 if is_url:
1345 new_val = gclient_utils.UpgradeToHttps(new_val)
1346 if new_val != initial:
1347 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001348
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001349 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001350 SetProperty(settings.GetDefaultPrivateFlag(),
1351 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001352 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001353 'tree-status-url', False)
1354 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +00001355 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
rmistry@google.com5626a922015-02-26 14:03:30 +00001356 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
1357 'run-post-upload-hook', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001358
1359 # TODO: configure a default branch to diff against, rather than this
1360 # svn-based hackery.
1361
1362
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001363class ChangeDescription(object):
1364 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001365 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00001366 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001367
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001368 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00001369 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001370
agable@chromium.org42c20792013-09-12 17:34:49 +00001371 @property # www.logilab.org/ticket/89786
1372 def description(self): # pylint: disable=E0202
1373 return '\n'.join(self._description_lines)
1374
1375 def set_description(self, desc):
1376 if isinstance(desc, basestring):
1377 lines = desc.splitlines()
1378 else:
1379 lines = [line.rstrip() for line in desc]
1380 while lines and not lines[0]:
1381 lines.pop(0)
1382 while lines and not lines[-1]:
1383 lines.pop(-1)
1384 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001385
piman@chromium.org336f9122014-09-04 02:16:55 +00001386 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001387 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001388 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001389 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001390 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001391 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001392
agable@chromium.org42c20792013-09-12 17:34:49 +00001393 # Get the set of R= and TBR= lines and remove them from the desciption.
1394 regexp = re.compile(self.R_LINE)
1395 matches = [regexp.match(line) for line in self._description_lines]
1396 new_desc = [l for i, l in enumerate(self._description_lines)
1397 if not matches[i]]
1398 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001399
agable@chromium.org42c20792013-09-12 17:34:49 +00001400 # Construct new unified R= and TBR= lines.
1401 r_names = []
1402 tbr_names = []
1403 for match in matches:
1404 if not match:
1405 continue
1406 people = cleanup_list([match.group(2).strip()])
1407 if match.group(1) == 'TBR':
1408 tbr_names.extend(people)
1409 else:
1410 r_names.extend(people)
1411 for name in r_names:
1412 if name not in reviewers:
1413 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001414 if add_owners_tbr:
1415 owners_db = owners.Database(change.RepositoryRoot(),
1416 fopen=file, os_path=os.path, glob=glob.glob)
1417 all_reviewers = set(tbr_names + reviewers)
1418 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1419 all_reviewers)
1420 tbr_names.extend(owners_db.reviewers_for(missing_files,
1421 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001422 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1423 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1424
1425 # Put the new lines in the description where the old first R= line was.
1426 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1427 if 0 <= line_loc < len(self._description_lines):
1428 if new_tbr_line:
1429 self._description_lines.insert(line_loc, new_tbr_line)
1430 if new_r_line:
1431 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001432 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001433 if new_r_line:
1434 self.append_footer(new_r_line)
1435 if new_tbr_line:
1436 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001437
1438 def prompt(self):
1439 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001440 self.set_description([
1441 '# Enter a description of the change.',
1442 '# This will be displayed on the codereview site.',
1443 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00001444 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00001445 '--------------------',
1446 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001447
agable@chromium.org42c20792013-09-12 17:34:49 +00001448 regexp = re.compile(self.BUG_LINE)
1449 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00001450 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00001451 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001452 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001453 if not content:
1454 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001455 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001456
1457 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001458 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1459 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001460 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001461 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001462
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001463 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001464 if self._description_lines:
1465 # Add an empty line if either the last line or the new line isn't a tag.
1466 last_line = self._description_lines[-1]
1467 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1468 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1469 self._description_lines.append('')
1470 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001471
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001472 def get_reviewers(self):
1473 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001474 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1475 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001476 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001477
1478
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001479def get_approving_reviewers(props):
1480 """Retrieves the reviewers that approved a CL from the issue properties with
1481 messages.
1482
1483 Note that the list may contain reviewers that are not committer, thus are not
1484 considered by the CQ.
1485 """
1486 return sorted(
1487 set(
1488 message['sender']
1489 for message in props['messages']
1490 if message['approval'] and message['sender'] in props['reviewers']
1491 )
1492 )
1493
1494
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001495def FindCodereviewSettingsFile(filename='codereview.settings'):
1496 """Finds the given file starting in the cwd and going up.
1497
1498 Only looks up to the top of the repository unless an
1499 'inherit-review-settings-ok' file exists in the root of the repository.
1500 """
1501 inherit_ok_file = 'inherit-review-settings-ok'
1502 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001503 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001504 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1505 root = '/'
1506 while True:
1507 if filename in os.listdir(cwd):
1508 if os.path.isfile(os.path.join(cwd, filename)):
1509 return open(os.path.join(cwd, filename))
1510 if cwd == root:
1511 break
1512 cwd = os.path.dirname(cwd)
1513
1514
1515def LoadCodereviewSettingsFromFile(fileobj):
1516 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001517 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001518
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001519 def SetProperty(name, setting, unset_error_ok=False):
1520 fullname = 'rietveld.' + name
1521 if setting in keyvals:
1522 RunGit(['config', fullname, keyvals[setting]])
1523 else:
1524 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1525
1526 SetProperty('server', 'CODE_REVIEW_SERVER')
1527 # Only server setting is required. Other settings can be absent.
1528 # In that case, we ignore errors raised during option deletion attempt.
1529 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001530 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001531 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1532 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001533 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001534 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001535 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
1536 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001537 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001538 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001539 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00001540 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
1541 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001542
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001543 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001544 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001545
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00001546 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
1547 RunGit(['config', 'gerrit.squash-uploads',
1548 keyvals['GERRIT_SQUASH_UPLOADS']])
1549
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001550 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1551 #should be of the form
1552 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1553 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1554 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1555 keyvals['ORIGIN_URL_CONFIG']])
1556
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001557
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001558def urlretrieve(source, destination):
1559 """urllib is broken for SSL connections via a proxy therefore we
1560 can't use urllib.urlretrieve()."""
1561 with open(destination, 'w') as f:
1562 f.write(urllib2.urlopen(source).read())
1563
1564
ukai@chromium.org712d6102013-11-27 00:52:58 +00001565def hasSheBang(fname):
1566 """Checks fname is a #! script."""
1567 with open(fname) as f:
1568 return f.read(2).startswith('#!')
1569
1570
tandrii@chromium.org18630d62016-03-04 12:06:02 +00001571def DownloadGerritHook(force):
1572 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001573
1574 Args:
1575 force: True to update hooks. False to install hooks if not present.
1576 """
1577 if not settings.GetIsGerrit():
1578 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001579 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001580 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1581 if not os.access(dst, os.X_OK):
1582 if os.path.exists(dst):
1583 if not force:
1584 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001585 try:
tandrii@chromium.org18630d62016-03-04 12:06:02 +00001586 print(
1587 'WARNING: installing Gerrit commit-msg hook.\n'
1588 ' This behavior of git cl will soon be disabled.\n'
1589 ' See bug http://crbug.com/579176.')
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001590 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001591 if not hasSheBang(dst):
1592 DieWithError('Not a script: %s\n'
1593 'You need to download from\n%s\n'
1594 'into .git/hooks/commit-msg and '
1595 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001596 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1597 except Exception:
1598 if os.path.exists(dst):
1599 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001600 DieWithError('\nFailed to download hooks.\n'
1601 'You need to download from\n%s\n'
1602 'into .git/hooks/commit-msg and '
1603 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001604
tandrii@chromium.orgc55295c2016-03-04 15:54:59 +00001605# TODO(tandrii): remove this once repos which call this method directly are
tandrii@chromium.org675bec32016-03-04 16:36:58 +00001606# upgraded. See http://crbug.com/579176.
tandrii@chromium.orgc55295c2016-03-04 15:54:59 +00001607DownloadHooks = DownloadGerritHook
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001608
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001609@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001610def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001611 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001612
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001613 parser.add_option('--activate-update', action='store_true',
1614 help='activate auto-updating [rietveld] section in '
1615 '.git/config')
1616 parser.add_option('--deactivate-update', action='store_true',
1617 help='deactivate auto-updating [rietveld] section in '
1618 '.git/config')
1619 options, args = parser.parse_args(args)
1620
1621 if options.deactivate_update:
1622 RunGit(['config', 'rietveld.autoupdate', 'false'])
1623 return
1624
1625 if options.activate_update:
1626 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1627 return
1628
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001629 if len(args) == 0:
1630 GetCodereviewSettingsInteractively()
tandrii@chromium.org18630d62016-03-04 12:06:02 +00001631 DownloadGerritHook(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001632 return 0
1633
1634 url = args[0]
1635 if not url.endswith('codereview.settings'):
1636 url = os.path.join(url, 'codereview.settings')
1637
1638 # Load code review settings and download hooks (if available).
1639 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
tandrii@chromium.org18630d62016-03-04 12:06:02 +00001640 DownloadGerritHook(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001641 return 0
1642
1643
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001644def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001645 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001646 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1647 branch = ShortBranchName(branchref)
1648 _, args = parser.parse_args(args)
1649 if not args:
1650 print("Current base-url:")
1651 return RunGit(['config', 'branch.%s.base-url' % branch],
1652 error_ok=False).strip()
1653 else:
1654 print("Setting base-url to %s" % args[0])
1655 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1656 error_ok=False).strip()
1657
1658
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001659def color_for_status(status):
1660 """Maps a Changelist status to color, for CMDstatus and other tools."""
1661 return {
1662 'unsent': Fore.RED,
1663 'waiting': Fore.BLUE,
1664 'reply': Fore.YELLOW,
1665 'lgtm': Fore.GREEN,
1666 'commit': Fore.MAGENTA,
1667 'closed': Fore.CYAN,
1668 'error': Fore.WHITE,
1669 }.get(status, Fore.WHITE)
1670
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001671def fetch_cl_status(branch, auth_config=None):
1672 """Fetches information for an issue and returns (branch, issue, status)."""
1673 cl = Changelist(branchref=branch, auth_config=auth_config)
1674 url = cl.GetIssueURL()
1675 status = cl.GetStatus()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001676
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001677 if url and (not status or status == 'error'):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001678 # The issue probably doesn't exist anymore.
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001679 url += ' (broken)'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001680
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001681 return (branch, url, status)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001682
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001683def get_cl_statuses(
1684 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001685 """Returns a blocking iterable of (branch, issue, color) for given branches.
1686
1687 If fine_grained is true, this will fetch CL statuses from the server.
1688 Otherwise, simply indicate if there's a matching url for the given branches.
1689
1690 If max_processes is specified, it is used as the maximum number of processes
1691 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
1692 spawned.
1693 """
1694 # Silence upload.py otherwise it becomes unwieldly.
1695 upload.verbosity = 0
1696
1697 if fine_grained:
1698 # Process one branch synchronously to work through authentication, then
1699 # spawn processes to process all the other branches in parallel.
1700 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001701 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
1702 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001703
1704 branches_to_fetch = branches[1:]
1705 pool = ThreadPool(
1706 min(max_processes, len(branches_to_fetch))
1707 if max_processes is not None
1708 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001709 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001710 yield x
1711 else:
1712 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1713 for b in branches:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001714 cl = Changelist(branchref=b, auth_config=auth_config)
1715 url = cl.GetIssueURL()
1716 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001717
rmistry@google.com2dd99862015-06-22 12:22:18 +00001718
1719def upload_branch_deps(cl, args):
1720 """Uploads CLs of local branches that are dependents of the current branch.
1721
1722 If the local branch dependency tree looks like:
1723 test1 -> test2.1 -> test3.1
1724 -> test3.2
1725 -> test2.2 -> test3.3
1726
1727 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
1728 run on the dependent branches in this order:
1729 test2.1, test3.1, test3.2, test2.2, test3.3
1730
1731 Note: This function does not rebase your local dependent branches. Use it when
1732 you make a change to the parent branch that will not conflict with its
1733 dependent branches, and you would like their dependencies updated in
1734 Rietveld.
1735 """
1736 if git_common.is_dirty_git_tree('upload-branch-deps'):
1737 return 1
1738
1739 root_branch = cl.GetBranch()
1740 if root_branch is None:
1741 DieWithError('Can\'t find dependent branches from detached HEAD state. '
1742 'Get on a branch!')
1743 if not cl.GetIssue() or not cl.GetPatchset():
1744 DieWithError('Current branch does not have an uploaded CL. We cannot set '
1745 'patchset dependencies without an uploaded CL.')
1746
1747 branches = RunGit(['for-each-ref',
1748 '--format=%(refname:short) %(upstream:short)',
1749 'refs/heads'])
1750 if not branches:
1751 print('No local branches found.')
1752 return 0
1753
1754 # Create a dictionary of all local branches to the branches that are dependent
1755 # on it.
1756 tracked_to_dependents = collections.defaultdict(list)
1757 for b in branches.splitlines():
1758 tokens = b.split()
1759 if len(tokens) == 2:
1760 branch_name, tracked = tokens
1761 tracked_to_dependents[tracked].append(branch_name)
1762
1763 print
1764 print 'The dependent local branches of %s are:' % root_branch
1765 dependents = []
1766 def traverse_dependents_preorder(branch, padding=''):
1767 dependents_to_process = tracked_to_dependents.get(branch, [])
1768 padding += ' '
1769 for dependent in dependents_to_process:
1770 print '%s%s' % (padding, dependent)
1771 dependents.append(dependent)
1772 traverse_dependents_preorder(dependent, padding)
1773 traverse_dependents_preorder(root_branch)
1774 print
1775
1776 if not dependents:
1777 print 'There are no dependent local branches for %s' % root_branch
1778 return 0
1779
1780 print ('This command will checkout all dependent branches and run '
1781 '"git cl upload".')
1782 ask_for_data('[Press enter to continue or ctrl-C to quit]')
1783
andybons@chromium.org962f9462016-02-03 20:00:42 +00001784 # Add a default patchset title to all upload calls in Rietveld.
1785 if not settings.GetIsGerrit():
1786 args.extend(['-t', 'Updated patchset dependency'])
1787
rmistry@google.com2dd99862015-06-22 12:22:18 +00001788 # Record all dependents that failed to upload.
1789 failures = {}
1790 # Go through all dependents, checkout the branch and upload.
1791 try:
1792 for dependent_branch in dependents:
1793 print
1794 print '--------------------------------------'
1795 print 'Running "git cl upload" from %s:' % dependent_branch
1796 RunGit(['checkout', '-q', dependent_branch])
1797 print
1798 try:
1799 if CMDupload(OptionParser(), args) != 0:
1800 print 'Upload failed for %s!' % dependent_branch
1801 failures[dependent_branch] = 1
1802 except: # pylint: disable=W0702
1803 failures[dependent_branch] = 1
1804 print
1805 finally:
1806 # Swap back to the original root branch.
1807 RunGit(['checkout', '-q', root_branch])
1808
1809 print
1810 print 'Upload complete for dependent branches!'
1811 for dependent_branch in dependents:
1812 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
1813 print ' %s : %s' % (dependent_branch, upload_status)
1814 print
1815
1816 return 0
1817
1818
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001819def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001820 """Show status of changelists.
1821
1822 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001823 - Red not sent for review or broken
1824 - Blue waiting for review
1825 - Yellow waiting for you to reply to review
1826 - Green LGTM'ed
1827 - Magenta in the commit queue
1828 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001829
1830 Also see 'git cl comments'.
1831 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001832 parser.add_option('--field',
1833 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001834 parser.add_option('-f', '--fast', action='store_true',
1835 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001836 parser.add_option(
1837 '-j', '--maxjobs', action='store', type=int,
1838 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001839
1840 auth.add_auth_options(parser)
1841 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001842 if args:
1843 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001844 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001845
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001846 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001847 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001848 if options.field.startswith('desc'):
1849 print cl.GetDescription()
1850 elif options.field == 'id':
1851 issueid = cl.GetIssue()
1852 if issueid:
1853 print issueid
1854 elif options.field == 'patch':
1855 patchset = cl.GetPatchset()
1856 if patchset:
1857 print patchset
1858 elif options.field == 'url':
1859 url = cl.GetIssueURL()
1860 if url:
1861 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001862 return 0
1863
1864 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1865 if not branches:
1866 print('No local branch found.')
1867 return 0
1868
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001869 changes = (
1870 Changelist(branchref=b, auth_config=auth_config)
1871 for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001872 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001873 alignment = max(5, max(len(b) for b in branches))
1874 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001875 output = get_cl_statuses(branches,
1876 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001877 max_processes=options.maxjobs,
1878 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001879
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001880 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001881 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001882 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001883 while branch not in branch_statuses:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001884 b, i, status = output.next()
1885 branch_statuses[b] = (i, status)
1886 issue_url, status = branch_statuses.pop(branch)
1887 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001888 reset = Fore.RESET
1889 if not sys.stdout.isatty():
1890 color = ''
1891 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001892 status_str = '(%s)' % status if status else ''
1893 print ' %*s : %s%s %s%s' % (
1894 alignment, ShortBranchName(branch), color, issue_url, status_str,
1895 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001896
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001897 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001898 print
1899 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001900 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00001901 if not cl.GetIssue():
1902 print 'No issue assigned.'
1903 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001904 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00001905 if not options.fast:
1906 print 'Issue description:'
1907 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001908 return 0
1909
1910
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001911def colorize_CMDstatus_doc():
1912 """To be called once in main() to add colors to git cl status help."""
1913 colors = [i for i in dir(Fore) if i[0].isupper()]
1914
1915 def colorize_line(line):
1916 for color in colors:
1917 if color in line.upper():
1918 # Extract whitespaces first and the leading '-'.
1919 indent = len(line) - len(line.lstrip(' ')) + 1
1920 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1921 return line
1922
1923 lines = CMDstatus.__doc__.splitlines()
1924 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1925
1926
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001927@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001928def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001929 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001930
1931 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001932 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00001933 parser.add_option('-r', '--reverse', action='store_true',
1934 help='Lookup the branch(es) for the specified issues. If '
1935 'no issues are specified, all branches with mapped '
1936 'issues will be listed.')
1937 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001938
dnj@chromium.org406c4402015-03-03 17:22:28 +00001939 if options.reverse:
1940 branches = RunGit(['for-each-ref', 'refs/heads',
1941 '--format=%(refname:short)']).splitlines()
1942
1943 # Reverse issue lookup.
1944 issue_branch_map = {}
1945 for branch in branches:
1946 cl = Changelist(branchref=branch)
1947 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
1948 if not args:
1949 args = sorted(issue_branch_map.iterkeys())
1950 for issue in args:
1951 if not issue:
1952 continue
1953 print 'Branch for issue number %s: %s' % (
1954 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
1955 else:
1956 cl = Changelist()
1957 if len(args) > 0:
1958 try:
1959 issue = int(args[0])
1960 except ValueError:
1961 DieWithError('Pass a number to set the issue or none to list it.\n'
1962 'Maybe you want to run git cl status?')
1963 cl.SetIssue(issue)
1964 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001965 return 0
1966
1967
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001968def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001969 """Shows or posts review comments for any changelist."""
1970 parser.add_option('-a', '--add-comment', dest='comment',
1971 help='comment to add to an issue')
1972 parser.add_option('-i', dest='issue',
1973 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00001974 parser.add_option('-j', '--json-file',
1975 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001976 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001977 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001978 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001979
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001980 issue = None
1981 if options.issue:
1982 try:
1983 issue = int(options.issue)
1984 except ValueError:
1985 DieWithError('A review issue id is expected to be a number')
1986
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001987 cl = Changelist(issue=issue, auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001988
1989 if options.comment:
1990 cl.AddComment(options.comment)
1991 return 0
1992
1993 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00001994 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00001995 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00001996 summary.append({
1997 'date': message['date'],
1998 'lgtm': False,
1999 'message': message['text'],
2000 'not_lgtm': False,
2001 'sender': message['sender'],
2002 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002003 if message['disapproval']:
2004 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00002005 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002006 elif message['approval']:
2007 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00002008 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002009 elif message['sender'] == data['owner_email']:
2010 color = Fore.MAGENTA
2011 else:
2012 color = Fore.BLUE
2013 print '\n%s%s %s%s' % (
2014 color, message['date'].split('.', 1)[0], message['sender'],
2015 Fore.RESET)
2016 if message['text'].strip():
2017 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00002018 if options.json_file:
2019 with open(options.json_file, 'wb') as f:
2020 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002021 return 0
2022
2023
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002024def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002025 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00002026 parser.add_option('-d', '--display', action='store_true',
2027 help='Display the description instead of opening an editor')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002028 auth.add_auth_options(parser)
2029 options, _ = parser.parse_args(args)
2030 auth_config = auth.extract_auth_config_from_options(options)
2031 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002032 if not cl.GetIssue():
2033 DieWithError('This branch has no associated changelist.')
2034 description = ChangeDescription(cl.GetDescription())
smut@google.com34fb6b12015-07-13 20:03:26 +00002035 if options.display:
2036 print description.description
2037 return 0
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002038 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00002039 if cl.GetDescription() != description.description:
2040 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002041 return 0
2042
2043
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002044def CreateDescriptionFromLog(args):
2045 """Pulls out the commit log to use as a base for the CL description."""
2046 log_args = []
2047 if len(args) == 1 and not args[0].endswith('.'):
2048 log_args = [args[0] + '..']
2049 elif len(args) == 1 and args[0].endswith('...'):
2050 log_args = [args[0][:-1]]
2051 elif len(args) == 2:
2052 log_args = [args[0] + '..' + args[1]]
2053 else:
2054 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00002055 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002056
2057
thestig@chromium.org44202a22014-03-11 19:22:18 +00002058def CMDlint(parser, args):
2059 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002060 parser.add_option('--filter', action='append', metavar='-x,+y',
2061 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002062 auth.add_auth_options(parser)
2063 options, args = parser.parse_args(args)
2064 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002065
2066 # Access to a protected member _XX of a client class
2067 # pylint: disable=W0212
2068 try:
2069 import cpplint
2070 import cpplint_chromium
2071 except ImportError:
2072 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
2073 return 1
2074
2075 # Change the current working directory before calling lint so that it
2076 # shows the correct base.
2077 previous_cwd = os.getcwd()
2078 os.chdir(settings.GetRoot())
2079 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002080 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002081 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
2082 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00002083 if not files:
2084 print "Cannot lint an empty CL"
2085 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00002086
2087 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002088 command = args + files
2089 if options.filter:
2090 command = ['--filter=' + ','.join(options.filter)] + command
2091 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002092
2093 white_regex = re.compile(settings.GetLintRegex())
2094 black_regex = re.compile(settings.GetLintIgnoreRegex())
2095 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
2096 for filename in filenames:
2097 if white_regex.match(filename):
2098 if black_regex.match(filename):
2099 print "Ignoring file %s" % filename
2100 else:
2101 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
2102 extra_check_functions)
2103 else:
2104 print "Skipping file %s" % filename
2105 finally:
2106 os.chdir(previous_cwd)
2107 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
2108 if cpplint._cpplint_state.error_count != 0:
2109 return 1
2110 return 0
2111
2112
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002113def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002114 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002115 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002116 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002117 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00002118 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002119 auth.add_auth_options(parser)
2120 options, args = parser.parse_args(args)
2121 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002122
sbc@chromium.org71437c02015-04-09 19:29:40 +00002123 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00002124 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002125 return 1
2126
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002127 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002128 if args:
2129 base_branch = args[0]
2130 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002131 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002132 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002133
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002134 cl.RunHook(
2135 committing=not options.upload,
2136 may_prompt=False,
2137 verbose=options.verbose,
2138 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00002139 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002140
2141
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002142def AddChangeIdToCommitMessage(options, args):
2143 """Re-commits using the current message, assumes the commit hook is in
2144 place.
2145 """
2146 log_desc = options.message or CreateDescriptionFromLog(args)
2147 git_command = ['commit', '--amend', '-m', log_desc]
2148 RunGit(git_command)
2149 new_log_desc = CreateDescriptionFromLog(args)
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00002150 if git_footers.get_footer_change_id(new_log_desc):
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002151 print 'git-cl: Added Change-Id to commit message.'
2152 else:
2153 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
2154
2155
tandrii@chromium.org65874e12016-03-04 12:03:02 +00002156def GenerateGerritChangeId(message):
2157 """Returns Ixxxxxx...xxx change id.
2158
2159 Works the same way as
2160 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
2161 but can be called on demand on all platforms.
2162
2163 The basic idea is to generate git hash of a state of the tree, original commit
2164 message, author/committer info and timestamps.
2165 """
2166 lines = []
2167 tree_hash = RunGitSilent(['write-tree'])
2168 lines.append('tree %s' % tree_hash.strip())
2169 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
2170 if code == 0:
2171 lines.append('parent %s' % parent.strip())
2172 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
2173 lines.append('author %s' % author.strip())
2174 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
2175 lines.append('committer %s' % committer.strip())
2176 lines.append('')
2177 # Note: Gerrit's commit-hook actually cleans message of some lines and
2178 # whitespace. This code is not doing this, but it clearly won't decrease
2179 # entropy.
2180 lines.append(message)
2181 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
2182 stdin='\n'.join(lines))
2183 return 'I%s' % change_hash.strip()
2184
2185
piman@chromium.org336f9122014-09-04 02:16:55 +00002186def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002187 """upload the current branch to gerrit."""
2188 # We assume the remote called "origin" is the one we want.
2189 # It is probably not worthwhile to support different workflows.
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002190 gerrit_remote = 'origin'
luqui@chromium.org609f3952015-05-04 22:47:04 +00002191
2192 remote, remote_branch = cl.GetRemoteBranch()
2193 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2194 pending_prefix='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002195
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002196 change_desc = ChangeDescription(
2197 options.message or CreateDescriptionFromLog(args))
2198 if not change_desc.description:
andybons@chromium.org962f9462016-02-03 20:00:42 +00002199 print "\nDescription is empty. Aborting..."
2200 return 1
2201
2202 if options.title:
2203 print "\nPatch titles (-t) are not supported in Gerrit. Aborting..."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002204 return 1
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002205
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002206 if options.squash:
2207 # Try to get the message from a previous upload.
2208 shadow_branch = 'refs/heads/git_cl_uploads/' + cl.GetBranch()
bauerb@chromium.org13502e02016-02-18 10:18:29 +00002209 message = RunGitSilent(['show', '--format=%B', '-s', shadow_branch])
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002210 if not message:
2211 if not options.force:
2212 change_desc.prompt()
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002213 if not change_desc.description:
2214 print "Description is empty; aborting."
2215 return 1
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002216 message = change_desc.description
tandrii@chromium.org57d86542016-03-04 16:11:32 +00002217 change_ids = git_footers.get_footer_change_id(message)
2218 if len(change_ids) > 1:
2219 DieWithError('too many Change-Id footers in %s branch' % shadow_branch)
2220 if not change_ids:
2221 message = git_footers.add_footer_change_id(
2222 message, GenerateGerritChangeId(message))
2223 change_desc.set_description(message)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002224
2225 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2226 if remote is '.':
2227 # If our upstream branch is local, we base our squashed commit on its
2228 # squashed version.
2229 parent = ('refs/heads/git_cl_uploads/' +
2230 scm.GIT.ShortBranchName(upstream_branch))
2231
2232 # Verify that the upstream branch has been uploaded too, otherwise Gerrit
2233 # will create additional CLs when uploading.
2234 if (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2235 RunGitSilent(['rev-parse', parent + ':'])):
2236 print 'Upload upstream branch ' + upstream_branch + ' first.'
2237 return 1
2238 else:
2239 parent = cl.GetCommonAncestorWithUpstream()
2240
2241 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2242 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2243 '-m', message]).strip()
2244 else:
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00002245 if not git_footers.get_footer_change_id(change_desc.description):
tandrii@chromium.org10625002016-03-04 20:03:47 +00002246 DownloadGerritHook(False)
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)