blob: a86ac021903c3774a829f35f4f3dffbe8317b60f [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
szager@chromium.org151ebcf2016-03-09 01:08:25 +000050import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000051import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000052import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000053import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000054import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000055import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000056import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000057import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000058import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000059import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000060import watchlists
61
maruel@chromium.org0633fb42013-08-16 20:06:14 +000062__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000063
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000064DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000065POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000066DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000067GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
rmistry@google.comc68112d2015-03-03 12:48:06 +000068REFS_THAT_ALIAS_TO_OTHER_REFS = {
69 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
70 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
71}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000072
thestig@chromium.org44202a22014-03-11 19:22:18 +000073# Valid extensions for files we want to lint.
74DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
75DEFAULT_LINT_IGNORE_REGEX = r"$^"
76
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000077# Shortcut since it quickly becomes redundant.
78Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000079
maruel@chromium.orgddd59412011-11-30 14:20:38 +000080# Initialized in main()
81settings = None
82
83
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000084def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000085 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000086 sys.exit(1)
87
88
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000089def GetNoGitPagerEnv():
90 env = os.environ.copy()
91 # 'cat' is a magical git string that disables pagers on all platforms.
92 env['GIT_PAGER'] = 'cat'
93 return env
94
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000095
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000096def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000097 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000098 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000099 except subprocess2.CalledProcessError as e:
100 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000101 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000102 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000103 'Command "%s" failed.\n%s' % (
104 ' '.join(args), error_message or e.stdout or ''))
105 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000106
107
108def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000109 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000110 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000111
112
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000113def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000114 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000115 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000116 if suppress_stderr:
117 stderr = subprocess2.VOID
118 else:
119 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000120 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000121 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000122 stdout=subprocess2.PIPE,
123 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000124 return code, out[0]
125 except ValueError:
126 # When the subprocess fails, it returns None. That triggers a ValueError
127 # when trying to unpack the return value into (out, code).
128 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000129
130
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000131def RunGitSilent(args):
132 """Returns stdout, suppresses stderr and ingores the return code."""
133 return RunGitWithCode(args, suppress_stderr=True)[1]
134
135
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000136def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000137 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000138 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000139 return (version.startswith(prefix) and
140 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000141
142
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000143def BranchExists(branch):
144 """Return True if specified branch exists."""
145 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
146 suppress_stderr=True)
147 return not code
148
149
maruel@chromium.org90541732011-04-01 17:54:18 +0000150def ask_for_data(prompt):
151 try:
152 return raw_input(prompt)
153 except KeyboardInterrupt:
154 # Hide the exception.
155 sys.exit(1)
156
157
iannucci@chromium.org79540052012-10-19 23:15:26 +0000158def git_set_branch_value(key, value):
159 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000160 if not branch:
161 return
162
163 cmd = ['config']
164 if isinstance(value, int):
165 cmd.append('--int')
166 git_key = 'branch.%s.%s' % (branch, key)
167 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000168
169
170def git_get_branch_default(key, default):
171 branch = Changelist().GetBranch()
172 if branch:
173 git_key = 'branch.%s.%s' % (branch, key)
174 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
175 try:
176 return int(stdout.strip())
177 except ValueError:
178 pass
179 return default
180
181
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000182def add_git_similarity(parser):
183 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000184 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000185 help='Sets the percentage that a pair of files need to match in order to'
186 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000187 parser.add_option(
188 '--find-copies', action='store_true',
189 help='Allows git to look for copies.')
190 parser.add_option(
191 '--no-find-copies', action='store_false', dest='find_copies',
192 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000193
194 old_parser_args = parser.parse_args
195 def Parse(args):
196 options, args = old_parser_args(args)
197
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000198 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000199 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000200 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000201 print('Note: Saving similarity of %d%% in git config.'
202 % options.similarity)
203 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000204
iannucci@chromium.org79540052012-10-19 23:15:26 +0000205 options.similarity = max(0, min(options.similarity, 100))
206
207 if options.find_copies is None:
208 options.find_copies = bool(
209 git_get_branch_default('git-find-copies', True))
210 else:
211 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000212
213 print('Using %d%% similarity for rename/copy detection. '
214 'Override with --similarity.' % options.similarity)
215
216 return options, args
217 parser.parse_args = Parse
218
219
machenbach@chromium.org45453142015-09-15 08:45:22 +0000220def _get_properties_from_options(options):
221 properties = dict(x.split('=', 1) for x in options.properties)
222 for key, val in properties.iteritems():
223 try:
224 properties[key] = json.loads(val)
225 except ValueError:
226 pass # If a value couldn't be evaluated, treat it as a string.
227 return properties
228
229
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000230def _prefix_master(master):
231 """Convert user-specified master name to full master name.
232
233 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
234 name, while the developers always use shortened master name
235 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
236 function does the conversion for buildbucket migration.
237 """
238 prefix = 'master.'
239 if master.startswith(prefix):
240 return master
241 return '%s%s' % (prefix, master)
242
243
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000244def _buildbucket_retry(operation_name, http, *args, **kwargs):
245 """Retries requests to buildbucket service and returns parsed json content."""
246 try_count = 0
247 while True:
248 response, content = http.request(*args, **kwargs)
249 try:
250 content_json = json.loads(content)
251 except ValueError:
252 content_json = None
253
254 # Buildbucket could return an error even if status==200.
255 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000256 error = content_json.get('error')
257 if error.get('code') == 403:
258 raise BuildbucketResponseException(
259 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000260 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000261 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000262 raise BuildbucketResponseException(msg)
263
264 if response.status == 200:
265 if not content_json:
266 raise BuildbucketResponseException(
267 'Buildbucket returns invalid json content: %s.\n'
268 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
269 content)
270 return content_json
271 if response.status < 500 or try_count >= 2:
272 raise httplib2.HttpLib2Error(content)
273
274 # status >= 500 means transient failures.
275 logging.debug('Transient errors when %s. Will retry.', operation_name)
276 time.sleep(0.5 + 1.5*try_count)
277 try_count += 1
278 assert False, 'unreachable'
279
280
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000281def trigger_luci_job(changelist, masters, options):
282 """Send a job to run on LUCI."""
283 issue_props = changelist.GetIssueProperties()
284 issue = changelist.GetIssue()
285 patchset = changelist.GetMostRecentPatchset()
286 for builders_and_tests in sorted(masters.itervalues()):
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000287 # TODO(hinoka et al): add support for other properties.
288 # Currently, this completely ignores testfilter and other properties.
289 for builder in sorted(builders_and_tests):
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000290 luci_trigger.trigger(
291 builder, 'HEAD', issue, patchset, issue_props['project'])
292
293
machenbach@chromium.org45453142015-09-15 08:45:22 +0000294def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000295 rietveld_url = settings.GetDefaultServerUrl()
296 rietveld_host = urlparse.urlparse(rietveld_url).hostname
297 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
298 http = authenticator.authorize(httplib2.Http())
299 http.force_exception_to_status_code = True
300 issue_props = changelist.GetIssueProperties()
301 issue = changelist.GetIssue()
302 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000303 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000304
305 buildbucket_put_url = (
306 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000307 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000308 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
309 hostname=rietveld_host,
310 issue=issue,
311 patch=patchset)
312
313 batch_req_body = {'builds': []}
314 print_text = []
315 print_text.append('Tried jobs on:')
316 for master, builders_and_tests in sorted(masters.iteritems()):
317 print_text.append('Master: %s' % master)
318 bucket = _prefix_master(master)
319 for builder, tests in sorted(builders_and_tests.iteritems()):
320 print_text.append(' %s: %s' % (builder, tests))
321 parameters = {
322 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000323 'changes': [{
324 'author': {'email': issue_props['owner_email']},
325 'revision': options.revision,
326 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000327 'properties': {
328 'category': category,
329 'issue': issue,
330 'master': master,
331 'patch_project': issue_props['project'],
332 'patch_storage': 'rietveld',
333 'patchset': patchset,
334 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000335 'rietveld': rietveld_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000336 },
337 }
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000338 if tests:
339 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000340 if properties:
341 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000342 if options.clobber:
343 parameters['properties']['clobber'] = True
344 batch_req_body['builds'].append(
345 {
346 'bucket': bucket,
347 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000348 'client_operation_id': str(uuid.uuid4()),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000349 'tags': ['builder:%s' % builder,
350 'buildset:%s' % buildset,
351 'master:%s' % master,
352 'user_agent:git_cl_try']
353 }
354 )
355
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000356 _buildbucket_retry(
357 'triggering tryjobs',
358 http,
359 buildbucket_put_url,
360 'PUT',
361 body=json.dumps(batch_req_body),
362 headers={'Content-Type': 'application/json'}
363 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000364 print_text.append('To see results here, run: git cl try-results')
365 print_text.append('To see results in browser, run: git cl web')
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000366 print '\n'.join(print_text)
kjellander@chromium.org44424542015-06-02 18:35:29 +0000367
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000368
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000369def fetch_try_jobs(auth_config, changelist, options):
370 """Fetches tryjobs from buildbucket.
371
372 Returns a map from build id to build info as json dictionary.
373 """
374 rietveld_url = settings.GetDefaultServerUrl()
375 rietveld_host = urlparse.urlparse(rietveld_url).hostname
376 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
377 if authenticator.has_cached_credentials():
378 http = authenticator.authorize(httplib2.Http())
379 else:
380 print ('Warning: Some results might be missing because %s' %
381 # Get the message on how to login.
382 auth.LoginRequiredError(rietveld_host).message)
383 http = httplib2.Http()
384
385 http.force_exception_to_status_code = True
386
387 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
388 hostname=rietveld_host,
389 issue=changelist.GetIssue(),
390 patch=options.patchset)
391 params = {'tag': 'buildset:%s' % buildset}
392
393 builds = {}
394 while True:
395 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
396 hostname=options.buildbucket_host,
397 params=urllib.urlencode(params))
398 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
399 for build in content.get('builds', []):
400 builds[build['id']] = build
401 if 'next_cursor' in content:
402 params['start_cursor'] = content['next_cursor']
403 else:
404 break
405 return builds
406
407
408def print_tryjobs(options, builds):
409 """Prints nicely result of fetch_try_jobs."""
410 if not builds:
411 print 'No tryjobs scheduled'
412 return
413
414 # Make a copy, because we'll be modifying builds dictionary.
415 builds = builds.copy()
416 builder_names_cache = {}
417
418 def get_builder(b):
419 try:
420 return builder_names_cache[b['id']]
421 except KeyError:
422 try:
423 parameters = json.loads(b['parameters_json'])
424 name = parameters['builder_name']
425 except (ValueError, KeyError) as error:
426 print 'WARNING: failed to get builder name for build %s: %s' % (
427 b['id'], error)
428 name = None
429 builder_names_cache[b['id']] = name
430 return name
431
432 def get_bucket(b):
433 bucket = b['bucket']
434 if bucket.startswith('master.'):
435 return bucket[len('master.'):]
436 return bucket
437
438 if options.print_master:
439 name_fmt = '%%-%ds %%-%ds' % (
440 max(len(str(get_bucket(b))) for b in builds.itervalues()),
441 max(len(str(get_builder(b))) for b in builds.itervalues()))
442 def get_name(b):
443 return name_fmt % (get_bucket(b), get_builder(b))
444 else:
445 name_fmt = '%%-%ds' % (
446 max(len(str(get_builder(b))) for b in builds.itervalues()))
447 def get_name(b):
448 return name_fmt % get_builder(b)
449
450 def sort_key(b):
451 return b['status'], b.get('result'), get_name(b), b.get('url')
452
453 def pop(title, f, color=None, **kwargs):
454 """Pop matching builds from `builds` dict and print them."""
455
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000456 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000457 colorize = str
458 else:
459 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
460
461 result = []
462 for b in builds.values():
463 if all(b.get(k) == v for k, v in kwargs.iteritems()):
464 builds.pop(b['id'])
465 result.append(b)
466 if result:
467 print colorize(title)
468 for b in sorted(result, key=sort_key):
469 print ' ', colorize('\t'.join(map(str, f(b))))
470
471 total = len(builds)
472 pop(status='COMPLETED', result='SUCCESS',
473 title='Successes:', color=Fore.GREEN,
474 f=lambda b: (get_name(b), b.get('url')))
475 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
476 title='Infra Failures:', color=Fore.MAGENTA,
477 f=lambda b: (get_name(b), b.get('url')))
478 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
479 title='Failures:', color=Fore.RED,
480 f=lambda b: (get_name(b), b.get('url')))
481 pop(status='COMPLETED', result='CANCELED',
482 title='Canceled:', color=Fore.MAGENTA,
483 f=lambda b: (get_name(b),))
484 pop(status='COMPLETED', result='FAILURE',
485 failure_reason='INVALID_BUILD_DEFINITION',
486 title='Wrong master/builder name:', color=Fore.MAGENTA,
487 f=lambda b: (get_name(b),))
488 pop(status='COMPLETED', result='FAILURE',
489 title='Other failures:',
490 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
491 pop(status='COMPLETED',
492 title='Other finished:',
493 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
494 pop(status='STARTED',
495 title='Started:', color=Fore.YELLOW,
496 f=lambda b: (get_name(b), b.get('url')))
497 pop(status='SCHEDULED',
498 title='Scheduled:',
499 f=lambda b: (get_name(b), 'id=%s' % b['id']))
500 # The last section is just in case buildbucket API changes OR there is a bug.
501 pop(title='Other:',
502 f=lambda b: (get_name(b), 'id=%s' % b['id']))
503 assert len(builds) == 0
504 print 'Total: %d tryjobs' % total
505
506
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000507def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
508 """Return the corresponding git ref if |base_url| together with |glob_spec|
509 matches the full |url|.
510
511 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
512 """
513 fetch_suburl, as_ref = glob_spec.split(':')
514 if allow_wildcards:
515 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
516 if glob_match:
517 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
518 # "branches/{472,597,648}/src:refs/remotes/svn/*".
519 branch_re = re.escape(base_url)
520 if glob_match.group(1):
521 branch_re += '/' + re.escape(glob_match.group(1))
522 wildcard = glob_match.group(2)
523 if wildcard == '*':
524 branch_re += '([^/]*)'
525 else:
526 # Escape and replace surrounding braces with parentheses and commas
527 # with pipe symbols.
528 wildcard = re.escape(wildcard)
529 wildcard = re.sub('^\\\\{', '(', wildcard)
530 wildcard = re.sub('\\\\,', '|', wildcard)
531 wildcard = re.sub('\\\\}$', ')', wildcard)
532 branch_re += wildcard
533 if glob_match.group(3):
534 branch_re += re.escape(glob_match.group(3))
535 match = re.match(branch_re, url)
536 if match:
537 return re.sub('\*$', match.group(1), as_ref)
538
539 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
540 if fetch_suburl:
541 full_url = base_url + '/' + fetch_suburl
542 else:
543 full_url = base_url
544 if full_url == url:
545 return as_ref
546 return None
547
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000548
iannucci@chromium.org79540052012-10-19 23:15:26 +0000549def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000550 """Prints statistics about the change to the user."""
551 # --no-ext-diff is broken in some versions of Git, so try to work around
552 # this by overriding the environment (but there is still a problem if the
553 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000554 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000555 if 'GIT_EXTERNAL_DIFF' in env:
556 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000557
558 if find_copies:
559 similarity_options = ['--find-copies-harder', '-l100000',
560 '-C%s' % similarity]
561 else:
562 similarity_options = ['-M%s' % similarity]
563
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000564 try:
565 stdout = sys.stdout.fileno()
566 except AttributeError:
567 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000568 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000569 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000570 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000571 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000572
573
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000574class BuildbucketResponseException(Exception):
575 pass
576
577
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000578class Settings(object):
579 def __init__(self):
580 self.default_server = None
581 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000582 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000583 self.is_git_svn = None
584 self.svn_branch = None
585 self.tree_status_url = None
586 self.viewvc_url = None
587 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000588 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000589 self.squash_gerrit_uploads = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000590 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000591 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000592 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000593 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000594
595 def LazyUpdateIfNeeded(self):
596 """Updates the settings from a codereview.settings file, if available."""
597 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000598 # The only value that actually changes the behavior is
599 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000600 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000601 error_ok=True
602 ).strip().lower()
603
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000604 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000605 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000606 LoadCodereviewSettingsFromFile(cr_settings_file)
607 self.updated = True
608
609 def GetDefaultServerUrl(self, error_ok=False):
610 if not self.default_server:
611 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000612 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000613 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000614 if error_ok:
615 return self.default_server
616 if not self.default_server:
617 error_message = ('Could not find settings file. You must configure '
618 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000619 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000620 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000621 return self.default_server
622
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000623 @staticmethod
624 def GetRelativeRoot():
625 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000626
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000627 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000628 if self.root is None:
629 self.root = os.path.abspath(self.GetRelativeRoot())
630 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000631
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000632 def GetGitMirror(self, remote='origin'):
633 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000634 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000635 if not os.path.isdir(local_url):
636 return None
637 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
638 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
639 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
640 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
641 if mirror.exists():
642 return mirror
643 return None
644
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000645 def GetIsGitSvn(self):
646 """Return true if this repo looks like it's using git-svn."""
647 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000648 if self.GetPendingRefPrefix():
649 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
650 self.is_git_svn = False
651 else:
652 # If you have any "svn-remote.*" config keys, we think you're using svn.
653 self.is_git_svn = RunGitWithCode(
654 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000655 return self.is_git_svn
656
657 def GetSVNBranch(self):
658 if self.svn_branch is None:
659 if not self.GetIsGitSvn():
660 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
661
662 # Try to figure out which remote branch we're based on.
663 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000664 # 1) iterate through our branch history and find the svn URL.
665 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000666
667 # regexp matching the git-svn line that contains the URL.
668 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
669
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000670 # We don't want to go through all of history, so read a line from the
671 # pipe at a time.
672 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000673 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000674 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
675 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000676 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000677 for line in proc.stdout:
678 match = git_svn_re.match(line)
679 if match:
680 url = match.group(1)
681 proc.stdout.close() # Cut pipe.
682 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000683
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000684 if url:
685 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
686 remotes = RunGit(['config', '--get-regexp',
687 r'^svn-remote\..*\.url']).splitlines()
688 for remote in remotes:
689 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000690 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000691 remote = match.group(1)
692 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000693 rewrite_root = RunGit(
694 ['config', 'svn-remote.%s.rewriteRoot' % remote],
695 error_ok=True).strip()
696 if rewrite_root:
697 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000698 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000699 ['config', 'svn-remote.%s.fetch' % remote],
700 error_ok=True).strip()
701 if fetch_spec:
702 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
703 if self.svn_branch:
704 break
705 branch_spec = RunGit(
706 ['config', 'svn-remote.%s.branches' % remote],
707 error_ok=True).strip()
708 if branch_spec:
709 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
710 if self.svn_branch:
711 break
712 tag_spec = RunGit(
713 ['config', 'svn-remote.%s.tags' % remote],
714 error_ok=True).strip()
715 if tag_spec:
716 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
717 if self.svn_branch:
718 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000719
720 if not self.svn_branch:
721 DieWithError('Can\'t guess svn branch -- try specifying it on the '
722 'command line')
723
724 return self.svn_branch
725
726 def GetTreeStatusUrl(self, error_ok=False):
727 if not self.tree_status_url:
728 error_message = ('You must configure your tree status URL by running '
729 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000730 self.tree_status_url = self._GetRietveldConfig(
731 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000732 return self.tree_status_url
733
734 def GetViewVCUrl(self):
735 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000736 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000737 return self.viewvc_url
738
rmistry@google.com90752582014-01-14 21:04:50 +0000739 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000740 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000741
rmistry@google.com78948ed2015-07-08 23:09:57 +0000742 def GetIsSkipDependencyUpload(self, branch_name):
743 """Returns true if specified branch should skip dep uploads."""
744 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
745 error_ok=True)
746
rmistry@google.com5626a922015-02-26 14:03:30 +0000747 def GetRunPostUploadHook(self):
748 run_post_upload_hook = self._GetRietveldConfig(
749 'run-post-upload-hook', error_ok=True)
750 return run_post_upload_hook == "True"
751
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000752 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000753 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000754
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000755 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000756 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000757
ukai@chromium.orge8077812012-02-03 03:41:46 +0000758 def GetIsGerrit(self):
759 """Return true if this repo is assosiated with gerrit code review system."""
760 if self.is_gerrit is None:
761 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
762 return self.is_gerrit
763
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000764 def GetSquashGerritUploads(self):
765 """Return true if uploads to Gerrit should be squashed by default."""
766 if self.squash_gerrit_uploads is None:
767 self.squash_gerrit_uploads = (
768 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
769 error_ok=True).strip() == 'true')
770 return self.squash_gerrit_uploads
771
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000772 def GetGitEditor(self):
773 """Return the editor specified in the git config, or None if none is."""
774 if self.git_editor is None:
775 self.git_editor = self._GetConfig('core.editor', error_ok=True)
776 return self.git_editor or None
777
thestig@chromium.org44202a22014-03-11 19:22:18 +0000778 def GetLintRegex(self):
779 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
780 DEFAULT_LINT_REGEX)
781
782 def GetLintIgnoreRegex(self):
783 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
784 DEFAULT_LINT_IGNORE_REGEX)
785
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000786 def GetProject(self):
787 if not self.project:
788 self.project = self._GetRietveldConfig('project', error_ok=True)
789 return self.project
790
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000791 def GetForceHttpsCommitUrl(self):
792 if not self.force_https_commit_url:
793 self.force_https_commit_url = self._GetRietveldConfig(
794 'force-https-commit-url', error_ok=True)
795 return self.force_https_commit_url
796
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000797 def GetPendingRefPrefix(self):
798 if not self.pending_ref_prefix:
799 self.pending_ref_prefix = self._GetRietveldConfig(
800 'pending-ref-prefix', error_ok=True)
801 return self.pending_ref_prefix
802
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000803 def _GetRietveldConfig(self, param, **kwargs):
804 return self._GetConfig('rietveld.' + param, **kwargs)
805
rmistry@google.com78948ed2015-07-08 23:09:57 +0000806 def _GetBranchConfig(self, branch_name, param, **kwargs):
807 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
808
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000809 def _GetConfig(self, param, **kwargs):
810 self.LazyUpdateIfNeeded()
811 return RunGit(['config', param], **kwargs).strip()
812
813
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000814def ShortBranchName(branch):
815 """Convert a name like 'refs/heads/foo' to just 'foo'."""
816 return branch.replace('refs/heads/', '')
817
818
819class Changelist(object):
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000820 def __init__(self, branchref=None, issue=None, auth_config=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000821 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000822 global settings
823 if not settings:
824 # Happens when git_cl.py is used as a utility library.
825 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000826 settings.GetDefaultServerUrl()
827 self.branchref = branchref
828 if self.branchref:
829 self.branch = ShortBranchName(self.branchref)
830 else:
831 self.branch = None
832 self.rietveld_server = None
833 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000834 self.lookedup_issue = False
835 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000836 self.has_description = False
837 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000838 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000839 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000840 self.cc = None
841 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000842 self._auth_config = auth_config
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000843 self._props = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000844 self._remote = None
845 self._rpc_server = None
846
847 @property
848 def auth_config(self):
849 return self._auth_config
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000850
851 def GetCCList(self):
852 """Return the users cc'd on this CL.
853
854 Return is a string suitable for passing to gcl with the --cc flag.
855 """
856 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000857 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000858 more_cc = ','.join(self.watchers)
859 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
860 return self.cc
861
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000862 def GetCCListWithoutDefault(self):
863 """Return the users cc'd on this CL excluding default ones."""
864 if self.cc is None:
865 self.cc = ','.join(self.watchers)
866 return self.cc
867
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000868 def SetWatchers(self, watchers):
869 """Set the list of email addresses that should be cc'd based on the changed
870 files in this CL.
871 """
872 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000873
874 def GetBranch(self):
875 """Returns the short branch name, e.g. 'master'."""
876 if not self.branch:
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000877 branchref = RunGit(['symbolic-ref', 'HEAD'],
878 stderr=subprocess2.VOID, error_ok=True).strip()
879 if not branchref:
880 return None
881 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000882 self.branch = ShortBranchName(self.branchref)
883 return self.branch
884
885 def GetBranchRef(self):
886 """Returns the full branch name, e.g. 'refs/heads/master'."""
887 self.GetBranch() # Poke the lazy loader.
888 return self.branchref
889
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000890 @staticmethod
891 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000892 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000893 e.g. 'origin', 'refs/heads/master'
894 """
895 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000896 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
897 error_ok=True).strip()
898 if upstream_branch:
899 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
900 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000901 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
902 error_ok=True).strip()
903 if upstream_branch:
904 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000905 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000906 # Fall back on trying a git-svn upstream branch.
907 if settings.GetIsGitSvn():
908 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000909 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000910 # Else, try to guess the origin remote.
911 remote_branches = RunGit(['branch', '-r']).split()
912 if 'origin/master' in remote_branches:
913 # Fall back on origin/master if it exits.
914 remote = 'origin'
915 upstream_branch = 'refs/heads/master'
916 elif 'origin/trunk' in remote_branches:
917 # Fall back on origin/trunk if it exists. Generally a shared
918 # git-svn clone
919 remote = 'origin'
920 upstream_branch = 'refs/heads/trunk'
921 else:
922 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000923Either pass complete "git diff"-style arguments, like
924 git cl upload origin/master
925or verify this branch is set up to track another (via the --track argument to
926"git checkout -b ...").""")
927
928 return remote, upstream_branch
929
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000930 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000931 upstream_branch = self.GetUpstreamBranch()
932 if not BranchExists(upstream_branch):
933 DieWithError('The upstream for the current branch (%s) does not exist '
934 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +0000935 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000936 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000937
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000938 def GetUpstreamBranch(self):
939 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000940 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000941 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000942 upstream_branch = upstream_branch.replace('refs/heads/',
943 'refs/remotes/%s/' % remote)
944 upstream_branch = upstream_branch.replace('refs/branch-heads/',
945 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000946 self.upstream_branch = upstream_branch
947 return self.upstream_branch
948
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000949 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000950 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000951 remote, branch = None, self.GetBranch()
952 seen_branches = set()
953 while branch not in seen_branches:
954 seen_branches.add(branch)
955 remote, branch = self.FetchUpstreamTuple(branch)
956 branch = ShortBranchName(branch)
957 if remote != '.' or branch.startswith('refs/remotes'):
958 break
959 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000960 remotes = RunGit(['remote'], error_ok=True).split()
961 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000962 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000963 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000964 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000965 logging.warning('Could not determine which remote this change is '
966 'associated with, so defaulting to "%s". This may '
967 'not be what you want. You may prevent this message '
968 'by running "git svn info" as documented here: %s',
969 self._remote,
970 GIT_INSTRUCTIONS_URL)
971 else:
972 logging.warn('Could not determine which remote this change is '
973 'associated with. You may prevent this message by '
974 'running "git svn info" as documented here: %s',
975 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000976 branch = 'HEAD'
977 if branch.startswith('refs/remotes'):
978 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000979 elif branch.startswith('refs/branch-heads/'):
980 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000981 else:
982 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000983 return self._remote
984
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000985 def GitSanityChecks(self, upstream_git_obj):
986 """Checks git repo status and ensures diff is from local commits."""
987
sbc@chromium.org79706062015-01-14 21:18:12 +0000988 if upstream_git_obj is None:
989 if self.GetBranch() is None:
990 print >> sys.stderr, (
dpranke@chromium.orgee87f582015-07-31 18:46:25 +0000991 'ERROR: unable to determine current branch (detached HEAD?)')
sbc@chromium.org79706062015-01-14 21:18:12 +0000992 else:
993 print >> sys.stderr, (
994 'ERROR: no upstream branch')
995 return False
996
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000997 # Verify the commit we're diffing against is in our current branch.
998 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
999 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1000 if upstream_sha != common_ancestor:
1001 print >> sys.stderr, (
1002 'ERROR: %s is not in the current branch. You may need to rebase '
1003 'your tracking branch' % upstream_sha)
1004 return False
1005
1006 # List the commits inside the diff, and verify they are all local.
1007 commits_in_diff = RunGit(
1008 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1009 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1010 remote_branch = remote_branch.strip()
1011 if code != 0:
1012 _, remote_branch = self.GetRemoteBranch()
1013
1014 commits_in_remote = RunGit(
1015 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1016
1017 common_commits = set(commits_in_diff) & set(commits_in_remote)
1018 if common_commits:
1019 print >> sys.stderr, (
1020 'ERROR: Your diff contains %d commits already in %s.\n'
1021 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1022 'the diff. If you are using a custom git flow, you can override'
1023 ' the reference used for this check with "git config '
1024 'gitcl.remotebranch <git-ref>".' % (
1025 len(common_commits), remote_branch, upstream_git_obj))
1026 return False
1027 return True
1028
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001029 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001030 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001031
1032 Returns None if it is not set.
1033 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001034 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
1035 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001036
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001037 def GetGitSvnRemoteUrl(self):
1038 """Return the configured git-svn remote URL parsed from git svn info.
1039
1040 Returns None if it is not set.
1041 """
1042 # URL is dependent on the current directory.
1043 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1044 if data:
1045 keys = dict(line.split(': ', 1) for line in data.splitlines()
1046 if ': ' in line)
1047 return keys.get('URL', None)
1048 return None
1049
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001050 def GetRemoteUrl(self):
1051 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1052
1053 Returns None if there is no remote.
1054 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001055 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001056 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1057
1058 # If URL is pointing to a local directory, it is probably a git cache.
1059 if os.path.isdir(url):
1060 url = RunGit(['config', 'remote.%s.url' % remote],
1061 error_ok=True,
1062 cwd=url).strip()
1063 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001064
1065 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001066 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001067 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001068 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001069 self.issue = int(issue) or None if issue else None
1070 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001071 return self.issue
1072
1073 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +00001074 if not self.rietveld_server:
1075 # If we're on a branch then get the server potentially associated
1076 # with that branch.
1077 if self.GetIssue():
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001078 rietveld_server_config = self._RietveldServer()
1079 if rietveld_server_config:
1080 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1081 ['config', rietveld_server_config], error_ok=True).strip())
evan@chromium.org0af9b702012-02-11 00:42:16 +00001082 if not self.rietveld_server:
1083 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001084 return self.rietveld_server
1085
1086 def GetIssueURL(self):
1087 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001088 if not self.GetIssue():
1089 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001090 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
1091
1092 def GetDescription(self, pretty=False):
1093 if not self.has_description:
1094 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +00001095 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +00001096 try:
1097 self.description = self.RpcServer().get_description(issue).strip()
maruel@chromium.org85616e02014-07-28 15:37:55 +00001098 except urllib2.HTTPError as e:
miket@chromium.org183df1a2012-01-04 19:44:55 +00001099 if e.code == 404:
1100 DieWithError(
1101 ('\nWhile fetching the description for issue %d, received a '
1102 '404 (not found)\n'
1103 'error. It is likely that you deleted this '
1104 'issue on the server. If this is the\n'
1105 'case, please run\n\n'
1106 ' git cl issue 0\n\n'
1107 'to clear the association with the deleted issue. Then run '
1108 'this command again.') % issue)
1109 else:
1110 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +00001111 '\nFailed to fetch issue description. HTTP error %d' % e.code)
maruel@chromium.org85616e02014-07-28 15:37:55 +00001112 except urllib2.URLError as e:
1113 print >> sys.stderr, (
1114 'Warning: Failed to retrieve CL description due to network '
1115 'failure.')
1116 self.description = ''
1117
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001118 self.has_description = True
1119 if pretty:
1120 wrapper = textwrap.TextWrapper()
1121 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1122 return wrapper.fill(self.description)
1123 return self.description
1124
1125 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001126 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001127 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001128 patchset = RunGit(['config', self._PatchsetSetting()],
1129 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001130 self.patchset = int(patchset) or None if patchset else None
1131 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001132 return self.patchset
1133
1134 def SetPatchset(self, patchset):
1135 """Set this branch's patchset. If patchset=0, clears the patchset."""
1136 if patchset:
1137 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001138 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001139 else:
1140 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001141 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001142 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001143
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001144 def GetMostRecentPatchset(self):
1145 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +00001146
1147 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001148 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001149 '/download/issue%s_%s.diff' % (issue, patchset))
1150
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001151 def GetIssueProperties(self):
1152 if self._props is None:
1153 issue = self.GetIssue()
1154 if not issue:
1155 self._props = {}
1156 else:
1157 self._props = self.RpcServer().get_issue_properties(issue, True)
1158 return self._props
1159
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001160 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001161 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001162
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001163 def AddComment(self, message):
1164 return self.RpcServer().add_comment(self.GetIssue(), message)
1165
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001166 def SetIssue(self, issue):
1167 """Set this branch's issue. If issue=0, clears the issue."""
1168 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001169 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001170 RunGit(['config', self._IssueSetting(), str(issue)])
1171 if self.rietveld_server:
1172 RunGit(['config', self._RietveldServer(), self.rietveld_server])
1173 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +00001174 current_issue = self.GetIssue()
1175 if current_issue:
1176 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001177 self.issue = None
1178 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001179
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001180 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001181 if not self.GitSanityChecks(upstream_branch):
1182 DieWithError('\nGit sanity check failure')
1183
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001184 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001185 if not root:
1186 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001187 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001188
1189 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001190 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001191 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001192 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001193 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001194 except subprocess2.CalledProcessError:
1195 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001196 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001197 'This branch probably doesn\'t exist anymore. To reset the\n'
1198 'tracking branch, please run\n'
1199 ' git branch --set-upstream %s trunk\n'
1200 'replacing trunk with origin/master or the relevant branch') %
1201 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001202
maruel@chromium.org52424302012-08-29 15:14:30 +00001203 issue = self.GetIssue()
1204 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001205 if issue:
1206 description = self.GetDescription()
1207 else:
1208 # If the change was never uploaded, use the log messages of all commits
1209 # up to the branch point, as git cl upload will prefill the description
1210 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001211 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1212 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001213
1214 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001215 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001216 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001217 name,
1218 description,
1219 absroot,
1220 files,
1221 issue,
1222 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001223 author,
1224 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001225
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001226 def GetStatus(self):
1227 """Apply a rough heuristic to give a simple summary of an issue's review
1228 or CQ status, assuming adherence to a common workflow.
1229
1230 Returns None if no issue for this branch, or one of the following keywords:
1231 * 'error' - error from review tool (including deleted issues)
1232 * 'unsent' - not sent for review
1233 * 'waiting' - waiting for review
1234 * 'reply' - waiting for owner to reply to review
1235 * 'lgtm' - LGTM from at least one approved reviewer
1236 * 'commit' - in the commit queue
1237 * 'closed' - closed
1238 """
1239 if not self.GetIssue():
1240 return None
1241
1242 try:
1243 props = self.GetIssueProperties()
1244 except urllib2.HTTPError:
1245 return 'error'
1246
1247 if props.get('closed'):
1248 # Issue is closed.
1249 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001250 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001251 # Issue is in the commit queue.
1252 return 'commit'
1253
1254 try:
1255 reviewers = self.GetApprovingReviewers()
1256 except urllib2.HTTPError:
1257 return 'error'
1258
1259 if reviewers:
1260 # Was LGTM'ed.
1261 return 'lgtm'
1262
1263 messages = props.get('messages') or []
1264
1265 if not messages:
1266 # No message was sent.
1267 return 'unsent'
1268 if messages[-1]['sender'] != props.get('owner_email'):
1269 # Non-LGTM reply from non-owner
1270 return 'reply'
1271 return 'waiting'
1272
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001273 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001274 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001275
1276 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001277 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001278 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001279 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001280 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001281 except presubmit_support.PresubmitFailure, e:
1282 DieWithError(
1283 ('%s\nMaybe your depot_tools is out of date?\n'
1284 'If all fails, contact maruel@') % e)
1285
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001286 def UpdateDescription(self, description):
1287 self.description = description
1288 return self.RpcServer().update_description(
1289 self.GetIssue(), self.description)
1290
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001291 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +00001292 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001293 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001294
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001295 def SetFlag(self, flag, value):
1296 """Patchset must match."""
1297 if not self.GetPatchset():
1298 DieWithError('The patchset needs to match. Send another patchset.')
1299 try:
1300 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001301 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001302 except urllib2.HTTPError, e:
1303 if e.code == 404:
1304 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1305 if e.code == 403:
1306 DieWithError(
1307 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1308 'match?') % (self.GetIssue(), self.GetPatchset()))
1309 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001310
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001311 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001312 """Returns an upload.RpcServer() to access this review's rietveld instance.
1313 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001314 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001315 self._rpc_server = rietveld.CachingRietveld(
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001316 self.GetRietveldServer(),
1317 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001318 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001319
1320 def _IssueSetting(self):
1321 """Return the git setting that stores this change's issue."""
1322 return 'branch.%s.rietveldissue' % self.GetBranch()
1323
1324 def _PatchsetSetting(self):
1325 """Return the git setting that stores this change's most recent patchset."""
1326 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1327
1328 def _RietveldServer(self):
1329 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001330 branch = self.GetBranch()
1331 if branch:
1332 return 'branch.%s.rietveldserver' % branch
1333 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001334
1335
1336def GetCodereviewSettingsInteractively():
1337 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001338 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001339 server = settings.GetDefaultServerUrl(error_ok=True)
1340 prompt = 'Rietveld server (host[:port])'
1341 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +00001342 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001343 if not server and not newserver:
1344 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001345 if newserver:
1346 newserver = gclient_utils.UpgradeToHttps(newserver)
1347 if newserver != server:
1348 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001349
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001350 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001351 prompt = caption
1352 if initial:
1353 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +00001354 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001355 if new_val == 'x':
1356 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001357 elif new_val:
1358 if is_url:
1359 new_val = gclient_utils.UpgradeToHttps(new_val)
1360 if new_val != initial:
1361 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001362
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001363 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001364 SetProperty(settings.GetDefaultPrivateFlag(),
1365 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001366 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001367 'tree-status-url', False)
1368 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +00001369 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
rmistry@google.com5626a922015-02-26 14:03:30 +00001370 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
1371 'run-post-upload-hook', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001372
1373 # TODO: configure a default branch to diff against, rather than this
1374 # svn-based hackery.
1375
1376
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001377class ChangeDescription(object):
1378 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001379 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00001380 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001381
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001382 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00001383 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001384
agable@chromium.org42c20792013-09-12 17:34:49 +00001385 @property # www.logilab.org/ticket/89786
1386 def description(self): # pylint: disable=E0202
1387 return '\n'.join(self._description_lines)
1388
1389 def set_description(self, desc):
1390 if isinstance(desc, basestring):
1391 lines = desc.splitlines()
1392 else:
1393 lines = [line.rstrip() for line in desc]
1394 while lines and not lines[0]:
1395 lines.pop(0)
1396 while lines and not lines[-1]:
1397 lines.pop(-1)
1398 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001399
piman@chromium.org336f9122014-09-04 02:16:55 +00001400 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001401 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001402 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001403 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001404 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001405 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001406
agable@chromium.org42c20792013-09-12 17:34:49 +00001407 # Get the set of R= and TBR= lines and remove them from the desciption.
1408 regexp = re.compile(self.R_LINE)
1409 matches = [regexp.match(line) for line in self._description_lines]
1410 new_desc = [l for i, l in enumerate(self._description_lines)
1411 if not matches[i]]
1412 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001413
agable@chromium.org42c20792013-09-12 17:34:49 +00001414 # Construct new unified R= and TBR= lines.
1415 r_names = []
1416 tbr_names = []
1417 for match in matches:
1418 if not match:
1419 continue
1420 people = cleanup_list([match.group(2).strip()])
1421 if match.group(1) == 'TBR':
1422 tbr_names.extend(people)
1423 else:
1424 r_names.extend(people)
1425 for name in r_names:
1426 if name not in reviewers:
1427 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001428 if add_owners_tbr:
1429 owners_db = owners.Database(change.RepositoryRoot(),
1430 fopen=file, os_path=os.path, glob=glob.glob)
1431 all_reviewers = set(tbr_names + reviewers)
1432 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1433 all_reviewers)
1434 tbr_names.extend(owners_db.reviewers_for(missing_files,
1435 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001436 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1437 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1438
1439 # Put the new lines in the description where the old first R= line was.
1440 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1441 if 0 <= line_loc < len(self._description_lines):
1442 if new_tbr_line:
1443 self._description_lines.insert(line_loc, new_tbr_line)
1444 if new_r_line:
1445 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001446 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001447 if new_r_line:
1448 self.append_footer(new_r_line)
1449 if new_tbr_line:
1450 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001451
1452 def prompt(self):
1453 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001454 self.set_description([
1455 '# Enter a description of the change.',
1456 '# This will be displayed on the codereview site.',
1457 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00001458 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00001459 '--------------------',
1460 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001461
agable@chromium.org42c20792013-09-12 17:34:49 +00001462 regexp = re.compile(self.BUG_LINE)
1463 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00001464 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00001465 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001466 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001467 if not content:
1468 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001469 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001470
1471 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001472 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1473 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001474 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001475 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001476
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001477 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001478 if self._description_lines:
1479 # Add an empty line if either the last line or the new line isn't a tag.
1480 last_line = self._description_lines[-1]
1481 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1482 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1483 self._description_lines.append('')
1484 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001485
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001486 def get_reviewers(self):
1487 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001488 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1489 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001490 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001491
1492
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001493def get_approving_reviewers(props):
1494 """Retrieves the reviewers that approved a CL from the issue properties with
1495 messages.
1496
1497 Note that the list may contain reviewers that are not committer, thus are not
1498 considered by the CQ.
1499 """
1500 return sorted(
1501 set(
1502 message['sender']
1503 for message in props['messages']
1504 if message['approval'] and message['sender'] in props['reviewers']
1505 )
1506 )
1507
1508
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001509def FindCodereviewSettingsFile(filename='codereview.settings'):
1510 """Finds the given file starting in the cwd and going up.
1511
1512 Only looks up to the top of the repository unless an
1513 'inherit-review-settings-ok' file exists in the root of the repository.
1514 """
1515 inherit_ok_file = 'inherit-review-settings-ok'
1516 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001517 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001518 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1519 root = '/'
1520 while True:
1521 if filename in os.listdir(cwd):
1522 if os.path.isfile(os.path.join(cwd, filename)):
1523 return open(os.path.join(cwd, filename))
1524 if cwd == root:
1525 break
1526 cwd = os.path.dirname(cwd)
1527
1528
1529def LoadCodereviewSettingsFromFile(fileobj):
1530 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001531 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001532
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001533 def SetProperty(name, setting, unset_error_ok=False):
1534 fullname = 'rietveld.' + name
1535 if setting in keyvals:
1536 RunGit(['config', fullname, keyvals[setting]])
1537 else:
1538 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1539
1540 SetProperty('server', 'CODE_REVIEW_SERVER')
1541 # Only server setting is required. Other settings can be absent.
1542 # In that case, we ignore errors raised during option deletion attempt.
1543 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001544 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001545 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1546 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001547 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001548 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001549 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
1550 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001551 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001552 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001553 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00001554 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
1555 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001556
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001557 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001558 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001559
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00001560 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
1561 RunGit(['config', 'gerrit.squash-uploads',
1562 keyvals['GERRIT_SQUASH_UPLOADS']])
1563
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001564 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1565 #should be of the form
1566 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1567 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1568 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1569 keyvals['ORIGIN_URL_CONFIG']])
1570
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001571
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001572def urlretrieve(source, destination):
1573 """urllib is broken for SSL connections via a proxy therefore we
1574 can't use urllib.urlretrieve()."""
1575 with open(destination, 'w') as f:
1576 f.write(urllib2.urlopen(source).read())
1577
1578
ukai@chromium.org712d6102013-11-27 00:52:58 +00001579def hasSheBang(fname):
1580 """Checks fname is a #! script."""
1581 with open(fname) as f:
1582 return f.read(2).startswith('#!')
1583
1584
tandrii@chromium.org18630d62016-03-04 12:06:02 +00001585def DownloadGerritHook(force):
1586 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001587
1588 Args:
1589 force: True to update hooks. False to install hooks if not present.
1590 """
1591 if not settings.GetIsGerrit():
1592 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001593 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001594 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1595 if not os.access(dst, os.X_OK):
1596 if os.path.exists(dst):
1597 if not force:
1598 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001599 try:
tandrii@chromium.org18630d62016-03-04 12:06:02 +00001600 print(
1601 'WARNING: installing Gerrit commit-msg hook.\n'
1602 ' This behavior of git cl will soon be disabled.\n'
1603 ' See bug http://crbug.com/579176.')
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001604 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001605 if not hasSheBang(dst):
1606 DieWithError('Not a script: %s\n'
1607 'You need to download from\n%s\n'
1608 'into .git/hooks/commit-msg and '
1609 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001610 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1611 except Exception:
1612 if os.path.exists(dst):
1613 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001614 DieWithError('\nFailed to download hooks.\n'
1615 'You need to download from\n%s\n'
1616 'into .git/hooks/commit-msg and '
1617 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001618
tandrii@chromium.orgc55295c2016-03-04 15:54:59 +00001619# TODO(tandrii): remove this once repos which call this method directly are
tandrii@chromium.org675bec32016-03-04 16:36:58 +00001620# upgraded. See http://crbug.com/579176.
tandrii@chromium.orgc55295c2016-03-04 15:54:59 +00001621DownloadHooks = DownloadGerritHook
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001622
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001623@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001624def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001625 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001626
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001627 parser.add_option('--activate-update', action='store_true',
1628 help='activate auto-updating [rietveld] section in '
1629 '.git/config')
1630 parser.add_option('--deactivate-update', action='store_true',
1631 help='deactivate auto-updating [rietveld] section in '
1632 '.git/config')
1633 options, args = parser.parse_args(args)
1634
1635 if options.deactivate_update:
1636 RunGit(['config', 'rietveld.autoupdate', 'false'])
1637 return
1638
1639 if options.activate_update:
1640 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1641 return
1642
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001643 if len(args) == 0:
1644 GetCodereviewSettingsInteractively()
tandrii@chromium.org18630d62016-03-04 12:06:02 +00001645 DownloadGerritHook(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001646 return 0
1647
1648 url = args[0]
1649 if not url.endswith('codereview.settings'):
1650 url = os.path.join(url, 'codereview.settings')
1651
1652 # Load code review settings and download hooks (if available).
1653 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
tandrii@chromium.org18630d62016-03-04 12:06:02 +00001654 DownloadGerritHook(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001655 return 0
1656
1657
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001658def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001659 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001660 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1661 branch = ShortBranchName(branchref)
1662 _, args = parser.parse_args(args)
1663 if not args:
1664 print("Current base-url:")
1665 return RunGit(['config', 'branch.%s.base-url' % branch],
1666 error_ok=False).strip()
1667 else:
1668 print("Setting base-url to %s" % args[0])
1669 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1670 error_ok=False).strip()
1671
1672
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001673def color_for_status(status):
1674 """Maps a Changelist status to color, for CMDstatus and other tools."""
1675 return {
1676 'unsent': Fore.RED,
1677 'waiting': Fore.BLUE,
1678 'reply': Fore.YELLOW,
1679 'lgtm': Fore.GREEN,
1680 'commit': Fore.MAGENTA,
1681 'closed': Fore.CYAN,
1682 'error': Fore.WHITE,
1683 }.get(status, Fore.WHITE)
1684
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001685def fetch_cl_status(branch, auth_config=None):
1686 """Fetches information for an issue and returns (branch, issue, status)."""
1687 cl = Changelist(branchref=branch, auth_config=auth_config)
1688 url = cl.GetIssueURL()
1689 status = cl.GetStatus()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001690
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001691 if url and (not status or status == 'error'):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001692 # The issue probably doesn't exist anymore.
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001693 url += ' (broken)'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001694
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001695 return (branch, url, status)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001696
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001697def get_cl_statuses(
1698 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001699 """Returns a blocking iterable of (branch, issue, color) for given branches.
1700
1701 If fine_grained is true, this will fetch CL statuses from the server.
1702 Otherwise, simply indicate if there's a matching url for the given branches.
1703
1704 If max_processes is specified, it is used as the maximum number of processes
1705 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
1706 spawned.
1707 """
1708 # Silence upload.py otherwise it becomes unwieldly.
1709 upload.verbosity = 0
1710
1711 if fine_grained:
1712 # Process one branch synchronously to work through authentication, then
1713 # spawn processes to process all the other branches in parallel.
1714 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001715 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
1716 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001717
1718 branches_to_fetch = branches[1:]
1719 pool = ThreadPool(
1720 min(max_processes, len(branches_to_fetch))
1721 if max_processes is not None
1722 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001723 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001724 yield x
1725 else:
1726 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1727 for b in branches:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001728 cl = Changelist(branchref=b, auth_config=auth_config)
1729 url = cl.GetIssueURL()
1730 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001731
rmistry@google.com2dd99862015-06-22 12:22:18 +00001732
1733def upload_branch_deps(cl, args):
1734 """Uploads CLs of local branches that are dependents of the current branch.
1735
1736 If the local branch dependency tree looks like:
1737 test1 -> test2.1 -> test3.1
1738 -> test3.2
1739 -> test2.2 -> test3.3
1740
1741 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
1742 run on the dependent branches in this order:
1743 test2.1, test3.1, test3.2, test2.2, test3.3
1744
1745 Note: This function does not rebase your local dependent branches. Use it when
1746 you make a change to the parent branch that will not conflict with its
1747 dependent branches, and you would like their dependencies updated in
1748 Rietveld.
1749 """
1750 if git_common.is_dirty_git_tree('upload-branch-deps'):
1751 return 1
1752
1753 root_branch = cl.GetBranch()
1754 if root_branch is None:
1755 DieWithError('Can\'t find dependent branches from detached HEAD state. '
1756 'Get on a branch!')
1757 if not cl.GetIssue() or not cl.GetPatchset():
1758 DieWithError('Current branch does not have an uploaded CL. We cannot set '
1759 'patchset dependencies without an uploaded CL.')
1760
1761 branches = RunGit(['for-each-ref',
1762 '--format=%(refname:short) %(upstream:short)',
1763 'refs/heads'])
1764 if not branches:
1765 print('No local branches found.')
1766 return 0
1767
1768 # Create a dictionary of all local branches to the branches that are dependent
1769 # on it.
1770 tracked_to_dependents = collections.defaultdict(list)
1771 for b in branches.splitlines():
1772 tokens = b.split()
1773 if len(tokens) == 2:
1774 branch_name, tracked = tokens
1775 tracked_to_dependents[tracked].append(branch_name)
1776
1777 print
1778 print 'The dependent local branches of %s are:' % root_branch
1779 dependents = []
1780 def traverse_dependents_preorder(branch, padding=''):
1781 dependents_to_process = tracked_to_dependents.get(branch, [])
1782 padding += ' '
1783 for dependent in dependents_to_process:
1784 print '%s%s' % (padding, dependent)
1785 dependents.append(dependent)
1786 traverse_dependents_preorder(dependent, padding)
1787 traverse_dependents_preorder(root_branch)
1788 print
1789
1790 if not dependents:
1791 print 'There are no dependent local branches for %s' % root_branch
1792 return 0
1793
1794 print ('This command will checkout all dependent branches and run '
1795 '"git cl upload".')
1796 ask_for_data('[Press enter to continue or ctrl-C to quit]')
1797
andybons@chromium.org962f9462016-02-03 20:00:42 +00001798 # Add a default patchset title to all upload calls in Rietveld.
1799 if not settings.GetIsGerrit():
1800 args.extend(['-t', 'Updated patchset dependency'])
1801
rmistry@google.com2dd99862015-06-22 12:22:18 +00001802 # Record all dependents that failed to upload.
1803 failures = {}
1804 # Go through all dependents, checkout the branch and upload.
1805 try:
1806 for dependent_branch in dependents:
1807 print
1808 print '--------------------------------------'
1809 print 'Running "git cl upload" from %s:' % dependent_branch
1810 RunGit(['checkout', '-q', dependent_branch])
1811 print
1812 try:
1813 if CMDupload(OptionParser(), args) != 0:
1814 print 'Upload failed for %s!' % dependent_branch
1815 failures[dependent_branch] = 1
1816 except: # pylint: disable=W0702
1817 failures[dependent_branch] = 1
1818 print
1819 finally:
1820 # Swap back to the original root branch.
1821 RunGit(['checkout', '-q', root_branch])
1822
1823 print
1824 print 'Upload complete for dependent branches!'
1825 for dependent_branch in dependents:
1826 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
1827 print ' %s : %s' % (dependent_branch, upload_status)
1828 print
1829
1830 return 0
1831
1832
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001833def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001834 """Show status of changelists.
1835
1836 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001837 - Red not sent for review or broken
1838 - Blue waiting for review
1839 - Yellow waiting for you to reply to review
1840 - Green LGTM'ed
1841 - Magenta in the commit queue
1842 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001843
1844 Also see 'git cl comments'.
1845 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001846 parser.add_option('--field',
1847 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001848 parser.add_option('-f', '--fast', action='store_true',
1849 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001850 parser.add_option(
1851 '-j', '--maxjobs', action='store', type=int,
1852 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001853
1854 auth.add_auth_options(parser)
1855 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001856 if args:
1857 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001858 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001859
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001860 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001861 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001862 if options.field.startswith('desc'):
1863 print cl.GetDescription()
1864 elif options.field == 'id':
1865 issueid = cl.GetIssue()
1866 if issueid:
1867 print issueid
1868 elif options.field == 'patch':
1869 patchset = cl.GetPatchset()
1870 if patchset:
1871 print patchset
1872 elif options.field == 'url':
1873 url = cl.GetIssueURL()
1874 if url:
1875 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001876 return 0
1877
1878 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1879 if not branches:
1880 print('No local branch found.')
1881 return 0
1882
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001883 changes = (
1884 Changelist(branchref=b, auth_config=auth_config)
1885 for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001886 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001887 alignment = max(5, max(len(b) for b in branches))
1888 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001889 output = get_cl_statuses(branches,
1890 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001891 max_processes=options.maxjobs,
1892 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001893
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001894 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001895 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001896 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001897 while branch not in branch_statuses:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001898 b, i, status = output.next()
1899 branch_statuses[b] = (i, status)
1900 issue_url, status = branch_statuses.pop(branch)
1901 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001902 reset = Fore.RESET
1903 if not sys.stdout.isatty():
1904 color = ''
1905 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001906 status_str = '(%s)' % status if status else ''
1907 print ' %*s : %s%s %s%s' % (
1908 alignment, ShortBranchName(branch), color, issue_url, status_str,
1909 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001910
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001911 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001912 print
1913 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001914 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00001915 if not cl.GetIssue():
1916 print 'No issue assigned.'
1917 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001918 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00001919 if not options.fast:
1920 print 'Issue description:'
1921 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001922 return 0
1923
1924
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001925def colorize_CMDstatus_doc():
1926 """To be called once in main() to add colors to git cl status help."""
1927 colors = [i for i in dir(Fore) if i[0].isupper()]
1928
1929 def colorize_line(line):
1930 for color in colors:
1931 if color in line.upper():
1932 # Extract whitespaces first and the leading '-'.
1933 indent = len(line) - len(line.lstrip(' ')) + 1
1934 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1935 return line
1936
1937 lines = CMDstatus.__doc__.splitlines()
1938 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1939
1940
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001941@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001942def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001943 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001944
1945 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001946 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00001947 parser.add_option('-r', '--reverse', action='store_true',
1948 help='Lookup the branch(es) for the specified issues. If '
1949 'no issues are specified, all branches with mapped '
1950 'issues will be listed.')
1951 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001952
dnj@chromium.org406c4402015-03-03 17:22:28 +00001953 if options.reverse:
1954 branches = RunGit(['for-each-ref', 'refs/heads',
1955 '--format=%(refname:short)']).splitlines()
1956
1957 # Reverse issue lookup.
1958 issue_branch_map = {}
1959 for branch in branches:
1960 cl = Changelist(branchref=branch)
1961 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
1962 if not args:
1963 args = sorted(issue_branch_map.iterkeys())
1964 for issue in args:
1965 if not issue:
1966 continue
1967 print 'Branch for issue number %s: %s' % (
1968 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
1969 else:
1970 cl = Changelist()
1971 if len(args) > 0:
1972 try:
1973 issue = int(args[0])
1974 except ValueError:
1975 DieWithError('Pass a number to set the issue or none to list it.\n'
1976 'Maybe you want to run git cl status?')
1977 cl.SetIssue(issue)
1978 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001979 return 0
1980
1981
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001982def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001983 """Shows or posts review comments for any changelist."""
1984 parser.add_option('-a', '--add-comment', dest='comment',
1985 help='comment to add to an issue')
1986 parser.add_option('-i', dest='issue',
1987 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00001988 parser.add_option('-j', '--json-file',
1989 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001990 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001991 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001992 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001993
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001994 issue = None
1995 if options.issue:
1996 try:
1997 issue = int(options.issue)
1998 except ValueError:
1999 DieWithError('A review issue id is expected to be a number')
2000
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002001 cl = Changelist(issue=issue, auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002002
2003 if options.comment:
2004 cl.AddComment(options.comment)
2005 return 0
2006
2007 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00002008 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00002009 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00002010 summary.append({
2011 'date': message['date'],
2012 'lgtm': False,
2013 'message': message['text'],
2014 'not_lgtm': False,
2015 'sender': message['sender'],
2016 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002017 if message['disapproval']:
2018 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00002019 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002020 elif message['approval']:
2021 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00002022 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002023 elif message['sender'] == data['owner_email']:
2024 color = Fore.MAGENTA
2025 else:
2026 color = Fore.BLUE
2027 print '\n%s%s %s%s' % (
2028 color, message['date'].split('.', 1)[0], message['sender'],
2029 Fore.RESET)
2030 if message['text'].strip():
2031 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00002032 if options.json_file:
2033 with open(options.json_file, 'wb') as f:
2034 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002035 return 0
2036
2037
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002038def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002039 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00002040 parser.add_option('-d', '--display', action='store_true',
2041 help='Display the description instead of opening an editor')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002042 auth.add_auth_options(parser)
2043 options, _ = parser.parse_args(args)
2044 auth_config = auth.extract_auth_config_from_options(options)
2045 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002046 if not cl.GetIssue():
2047 DieWithError('This branch has no associated changelist.')
2048 description = ChangeDescription(cl.GetDescription())
smut@google.com34fb6b12015-07-13 20:03:26 +00002049 if options.display:
2050 print description.description
2051 return 0
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002052 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00002053 if cl.GetDescription() != description.description:
2054 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002055 return 0
2056
2057
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002058def CreateDescriptionFromLog(args):
2059 """Pulls out the commit log to use as a base for the CL description."""
2060 log_args = []
2061 if len(args) == 1 and not args[0].endswith('.'):
2062 log_args = [args[0] + '..']
2063 elif len(args) == 1 and args[0].endswith('...'):
2064 log_args = [args[0][:-1]]
2065 elif len(args) == 2:
2066 log_args = [args[0] + '..' + args[1]]
2067 else:
2068 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00002069 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002070
2071
thestig@chromium.org44202a22014-03-11 19:22:18 +00002072def CMDlint(parser, args):
2073 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002074 parser.add_option('--filter', action='append', metavar='-x,+y',
2075 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002076 auth.add_auth_options(parser)
2077 options, args = parser.parse_args(args)
2078 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002079
2080 # Access to a protected member _XX of a client class
2081 # pylint: disable=W0212
2082 try:
2083 import cpplint
2084 import cpplint_chromium
2085 except ImportError:
2086 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
2087 return 1
2088
2089 # Change the current working directory before calling lint so that it
2090 # shows the correct base.
2091 previous_cwd = os.getcwd()
2092 os.chdir(settings.GetRoot())
2093 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002094 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002095 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
2096 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00002097 if not files:
2098 print "Cannot lint an empty CL"
2099 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00002100
2101 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002102 command = args + files
2103 if options.filter:
2104 command = ['--filter=' + ','.join(options.filter)] + command
2105 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002106
2107 white_regex = re.compile(settings.GetLintRegex())
2108 black_regex = re.compile(settings.GetLintIgnoreRegex())
2109 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
2110 for filename in filenames:
2111 if white_regex.match(filename):
2112 if black_regex.match(filename):
2113 print "Ignoring file %s" % filename
2114 else:
2115 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
2116 extra_check_functions)
2117 else:
2118 print "Skipping file %s" % filename
2119 finally:
2120 os.chdir(previous_cwd)
2121 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
2122 if cpplint._cpplint_state.error_count != 0:
2123 return 1
2124 return 0
2125
2126
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002127def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002128 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002129 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002130 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002131 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00002132 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002133 auth.add_auth_options(parser)
2134 options, args = parser.parse_args(args)
2135 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002136
sbc@chromium.org71437c02015-04-09 19:29:40 +00002137 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00002138 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002139 return 1
2140
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002141 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002142 if args:
2143 base_branch = args[0]
2144 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002145 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002146 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002147
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002148 cl.RunHook(
2149 committing=not options.upload,
2150 may_prompt=False,
2151 verbose=options.verbose,
2152 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00002153 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002154
2155
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002156def AddChangeIdToCommitMessage(options, args):
2157 """Re-commits using the current message, assumes the commit hook is in
2158 place.
2159 """
2160 log_desc = options.message or CreateDescriptionFromLog(args)
2161 git_command = ['commit', '--amend', '-m', log_desc]
2162 RunGit(git_command)
2163 new_log_desc = CreateDescriptionFromLog(args)
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00002164 if git_footers.get_footer_change_id(new_log_desc):
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002165 print 'git-cl: Added Change-Id to commit message.'
2166 else:
2167 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
2168
2169
tandrii@chromium.org65874e12016-03-04 12:03:02 +00002170def GenerateGerritChangeId(message):
2171 """Returns Ixxxxxx...xxx change id.
2172
2173 Works the same way as
2174 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
2175 but can be called on demand on all platforms.
2176
2177 The basic idea is to generate git hash of a state of the tree, original commit
2178 message, author/committer info and timestamps.
2179 """
2180 lines = []
2181 tree_hash = RunGitSilent(['write-tree'])
2182 lines.append('tree %s' % tree_hash.strip())
2183 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
2184 if code == 0:
2185 lines.append('parent %s' % parent.strip())
2186 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
2187 lines.append('author %s' % author.strip())
2188 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
2189 lines.append('committer %s' % committer.strip())
2190 lines.append('')
2191 # Note: Gerrit's commit-hook actually cleans message of some lines and
2192 # whitespace. This code is not doing this, but it clearly won't decrease
2193 # entropy.
2194 lines.append(message)
2195 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
2196 stdin='\n'.join(lines))
2197 return 'I%s' % change_hash.strip()
2198
2199
piman@chromium.org336f9122014-09-04 02:16:55 +00002200def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002201 """upload the current branch to gerrit."""
2202 # We assume the remote called "origin" is the one we want.
2203 # It is probably not worthwhile to support different workflows.
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002204 gerrit_remote = 'origin'
luqui@chromium.org609f3952015-05-04 22:47:04 +00002205
2206 remote, remote_branch = cl.GetRemoteBranch()
2207 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2208 pending_prefix='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002209
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002210 change_desc = ChangeDescription(
2211 options.message or CreateDescriptionFromLog(args))
2212 if not change_desc.description:
andybons@chromium.org962f9462016-02-03 20:00:42 +00002213 print "\nDescription is empty. Aborting..."
2214 return 1
2215
2216 if options.title:
2217 print "\nPatch titles (-t) are not supported in Gerrit. Aborting..."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002218 return 1
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002219
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002220 if options.squash:
2221 # Try to get the message from a previous upload.
2222 shadow_branch = 'refs/heads/git_cl_uploads/' + cl.GetBranch()
bauerb@chromium.org13502e02016-02-18 10:18:29 +00002223 message = RunGitSilent(['show', '--format=%B', '-s', shadow_branch])
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002224 if not message:
2225 if not options.force:
2226 change_desc.prompt()
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002227 if not change_desc.description:
2228 print "Description is empty; aborting."
2229 return 1
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002230 message = change_desc.description
tandrii@chromium.org57d86542016-03-04 16:11:32 +00002231 change_ids = git_footers.get_footer_change_id(message)
2232 if len(change_ids) > 1:
2233 DieWithError('too many Change-Id footers in %s branch' % shadow_branch)
2234 if not change_ids:
2235 message = git_footers.add_footer_change_id(
2236 message, GenerateGerritChangeId(message))
2237 change_desc.set_description(message)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002238
2239 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2240 if remote is '.':
2241 # If our upstream branch is local, we base our squashed commit on its
2242 # squashed version.
2243 parent = ('refs/heads/git_cl_uploads/' +
2244 scm.GIT.ShortBranchName(upstream_branch))
2245
2246 # Verify that the upstream branch has been uploaded too, otherwise Gerrit
2247 # will create additional CLs when uploading.
2248 if (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2249 RunGitSilent(['rev-parse', parent + ':'])):
2250 print 'Upload upstream branch ' + upstream_branch + ' first.'
2251 return 1
2252 else:
2253 parent = cl.GetCommonAncestorWithUpstream()
2254
2255 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2256 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2257 '-m', message]).strip()
2258 else:
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00002259 if not git_footers.get_footer_change_id(change_desc.description):
tandrii@chromium.org10625002016-03-04 20:03:47 +00002260 DownloadGerritHook(False)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002261 AddChangeIdToCommitMessage(options, args)
2262 ref_to_push = 'HEAD'
2263 parent = '%s/%s' % (gerrit_remote, branch)
2264
2265 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2266 ref_to_push)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002267 if len(commits) > 1:
2268 print('WARNING: This will upload %d commits. Run the following command '
2269 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002270 print('git log %s..%s' % (parent, ref_to_push))
2271 print('You can also use `git squash-branch` to squash these into a single '
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002272 'commit.')
2273 ask_for_data('About to upload; enter to confirm.')
2274
piman@chromium.org336f9122014-09-04 02:16:55 +00002275 if options.reviewers or options.tbr_owners:
2276 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002277
ukai@chromium.orge8077812012-02-03 03:41:46 +00002278 receive_options = []
2279 cc = cl.GetCCList().split(',')
2280 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002281 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002282 cc = filter(None, cc)
2283 if cc:
2284 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002285 if change_desc.get_reviewers():
2286 receive_options.extend(
2287 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002288
ukai@chromium.orge8077812012-02-03 03:41:46 +00002289 git_command = ['push']
2290 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00002291 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00002292 ' '.join(receive_options))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002293 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
ukai@chromium.orge8077812012-02-03 03:41:46 +00002294 RunGit(git_command)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002295
2296 if options.squash:
2297 head = RunGit(['rev-parse', 'HEAD']).strip()
2298 RunGit(['update-ref', '-m', 'Uploaded ' + head, shadow_branch, ref_to_push])
2299
ukai@chromium.orge8077812012-02-03 03:41:46 +00002300 # TODO(ukai): parse Change-Id: and set issue number?
2301 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002302
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002303
wittman@chromium.org455dc922015-01-26 20:15:50 +00002304def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
2305 """Computes the remote branch ref to use for the CL.
2306
2307 Args:
2308 remote (str): The git remote for the CL.
2309 remote_branch (str): The git remote branch for the CL.
2310 target_branch (str): The target branch specified by the user.
2311 pending_prefix (str): The pending prefix from the settings.
2312 """
2313 if not (remote and remote_branch):
2314 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002315
wittman@chromium.org455dc922015-01-26 20:15:50 +00002316 if target_branch:
2317 # Cannonicalize branch references to the equivalent local full symbolic
2318 # refs, which are then translated into the remote full symbolic refs
2319 # below.
2320 if '/' not in target_branch:
2321 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
2322 else:
2323 prefix_replacements = (
2324 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
2325 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
2326 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
2327 )
2328 match = None
2329 for regex, replacement in prefix_replacements:
2330 match = re.search(regex, target_branch)
2331 if match:
2332 remote_branch = target_branch.replace(match.group(0), replacement)
2333 break
2334 if not match:
2335 # This is a branch path but not one we recognize; use as-is.
2336 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00002337 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
2338 # Handle the refs that need to land in different refs.
2339 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002340
wittman@chromium.org455dc922015-01-26 20:15:50 +00002341 # Create the true path to the remote branch.
2342 # Does the following translation:
2343 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
2344 # * refs/remotes/origin/master -> refs/heads/master
2345 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
2346 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
2347 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
2348 elif remote_branch.startswith('refs/remotes/%s/' % remote):
2349 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
2350 'refs/heads/')
2351 elif remote_branch.startswith('refs/remotes/branch-heads'):
2352 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
2353 # If a pending prefix exists then replace refs/ with it.
2354 if pending_prefix:
2355 remote_branch = remote_branch.replace('refs/', pending_prefix)
2356 return remote_branch
2357
2358
piman@chromium.org336f9122014-09-04 02:16:55 +00002359def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002360 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002361 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2362 upload_args.extend(['--server', cl.GetRietveldServer()])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002363 upload_args.extend(auth.auth_config_to_command_options(cl.auth_config))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002364 if options.emulate_svn_auto_props:
2365 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002366
2367 change_desc = None
2368
pgervais@chromium.org91141372014-01-09 23:27:20 +00002369 if options.email is not None:
2370 upload_args.extend(['--email', options.email])
2371
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002372 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002373 if options.title:
2374 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00002375 if options.message:
2376 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00002377 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002378 print ("This branch is associated with issue %s. "
2379 "Adding patch to that issue." % cl.GetIssue())
2380 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002381 if options.title:
2382 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00002383 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002384 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00002385 if options.reviewers or options.tbr_owners:
2386 change_desc.update_reviewers(options.reviewers,
2387 options.tbr_owners,
2388 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002389 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002390 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002391
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002392 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002393 print "Description is empty; aborting."
2394 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002395
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002396 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002397 if change_desc.get_reviewers():
2398 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00002399 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002400 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00002401 DieWithError("Must specify reviewers to send email.")
2402 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00002403
2404 # We check this before applying rietveld.private assuming that in
2405 # rietveld.cc only addresses which we can send private CLs to are listed
2406 # if rietveld.private is set, and so we should ignore rietveld.cc only when
2407 # --private is specified explicitly on the command line.
2408 if options.private:
2409 logging.warn('rietveld.cc is ignored since private flag is specified. '
2410 'You need to review and add them manually if necessary.')
2411 cc = cl.GetCCListWithoutDefault()
2412 else:
2413 cc = cl.GetCCList()
2414 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00002415 if cc:
2416 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002417
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002418 if options.private or settings.GetDefaultPrivateFlag() == "True":
2419 upload_args.append('--private')
2420
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002421 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00002422 if not options.find_copies:
2423 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002424
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002425 # Include the upstream repo's URL in the change -- this is useful for
2426 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002427 remote_url = cl.GetGitBaseUrlFromConfig()
2428 if not remote_url:
2429 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002430 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002431 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00002432 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
2433 remote_url = (cl.GetRemoteUrl() + '@'
2434 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002435 if remote_url:
2436 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00002437 remote, remote_branch = cl.GetRemoteBranch()
wittman@chromium.org455dc922015-01-26 20:15:50 +00002438 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2439 settings.GetPendingRefPrefix())
2440 if target_ref:
2441 upload_args.extend(['--target_ref', target_ref])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002442
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002443 # Look for dependent patchsets. See crbug.com/480453 for more details.
2444 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2445 upstream_branch = ShortBranchName(upstream_branch)
2446 if remote is '.':
2447 # A local branch is being tracked.
2448 local_branch = ShortBranchName(upstream_branch)
rmistry@google.com78948ed2015-07-08 23:09:57 +00002449 if settings.GetIsSkipDependencyUpload(local_branch):
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002450 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002451 print ('Skipping dependency patchset upload because git config '
2452 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002453 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002454 else:
2455 auth_config = auth.extract_auth_config_from_options(options)
2456 branch_cl = Changelist(branchref=local_branch, auth_config=auth_config)
2457 branch_cl_issue_url = branch_cl.GetIssueURL()
2458 branch_cl_issue = branch_cl.GetIssue()
2459 branch_cl_patchset = branch_cl.GetPatchset()
2460 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2461 upload_args.extend(
2462 ['--depends_on_patchset', '%s:%s' % (
2463 branch_cl_issue, branch_cl_patchset)])
2464 print
2465 print ('The current branch (%s) is tracking a local branch (%s) with '
2466 'an associated CL.') % (cl.GetBranch(), local_branch)
2467 print 'Adding %s/#ps%s as a dependency patchset.' % (
2468 branch_cl_issue_url, branch_cl_patchset)
2469 print
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002470
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002471 project = settings.GetProject()
2472 if project:
2473 upload_args.extend(['--project', project])
2474
rmistry@google.comef966222015-04-07 11:15:01 +00002475 if options.cq_dry_run:
2476 upload_args.extend(['--cq_dry_run'])
2477
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002478 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00002479 upload_args = ['upload'] + upload_args + args
2480 logging.info('upload.RealMain(%s)', upload_args)
2481 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00002482 issue = int(issue)
2483 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00002484 except KeyboardInterrupt:
2485 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002486 except:
2487 # If we got an exception after the user typed a description for their
2488 # change, back up the description before re-raising.
2489 if change_desc:
2490 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2491 print '\nGot exception while uploading -- saving description to %s\n' \
2492 % backup_path
2493 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002494 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002495 backup_file.close()
2496 raise
2497
2498 if not cl.GetIssue():
2499 cl.SetIssue(issue)
2500 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002501
2502 if options.use_commit_queue:
2503 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002504 return 0
2505
2506
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002507def cleanup_list(l):
2508 """Fixes a list so that comma separated items are put as individual items.
2509
2510 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
2511 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
2512 """
2513 items = sum((i.split(',') for i in l), [])
2514 stripped_items = (i.strip() for i in items)
2515 return sorted(filter(None, stripped_items))
2516
2517
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002518@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002519def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00002520 """Uploads the current changelist to codereview.
2521
2522 Can skip dependency patchset uploads for a branch by running:
2523 git config branch.branch_name.skip-deps-uploads True
2524 To unset run:
2525 git config --unset branch.branch_name.skip-deps-uploads
2526 Can also set the above globally by using the --global flag.
2527 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00002528 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2529 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002530 parser.add_option('--bypass-watchlists', action='store_true',
2531 dest='bypass_watchlists',
2532 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002533 parser.add_option('-f', action='store_true', dest='force',
2534 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002535 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00002536 parser.add_option('-t', dest='title',
2537 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002538 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002539 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002540 help='reviewer email addresses')
2541 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002542 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002543 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00002544 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00002545 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002546 parser.add_option('--emulate_svn_auto_props',
2547 '--emulate-svn-auto-props',
2548 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00002549 dest="emulate_svn_auto_props",
2550 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00002551 parser.add_option('-c', '--use-commit-queue', action='store_true',
2552 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002553 parser.add_option('--private', action='store_true',
2554 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00002555 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002556 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00002557 metavar='TARGET',
2558 help='Apply CL to remote ref TARGET. ' +
2559 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002560 parser.add_option('--squash', action='store_true',
2561 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002562 parser.add_option('--no-squash', action='store_true',
2563 help='Don\'t squash multiple commits into one ' +
2564 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002565 parser.add_option('--email', default=None,
2566 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00002567 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
2568 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00002569 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
2570 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00002571 help='Send the patchset to do a CQ dry run right after '
2572 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00002573 parser.add_option('--dependencies', action='store_true',
2574 help='Uploads CLs of all the local branches that depend on '
2575 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002576
rmistry@google.com2dd99862015-06-22 12:22:18 +00002577 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002578 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002579 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002580 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002581 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002582
sbc@chromium.org71437c02015-04-09 19:29:40 +00002583 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002584 return 1
2585
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002586 options.reviewers = cleanup_list(options.reviewers)
2587 options.cc = cleanup_list(options.cc)
2588
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002589 cl = Changelist(auth_config=auth_config)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002590 if args:
2591 # TODO(ukai): is it ok for gerrit case?
2592 base_branch = args[0]
2593 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00002594 if cl.GetBranch() is None:
2595 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
2596
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002597 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002598 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00002599 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00002600
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00002601 # Make sure authenticated to Rietveld before running expensive hooks. It is
2602 # a fast, best efforts check. Rietveld still can reject the authentication
2603 # during the actual upload.
2604 if not settings.GetIsGerrit() and auth_config.use_oauth2:
2605 authenticator = auth.get_authenticator_for_host(
2606 cl.GetRietveldServer(), auth_config)
2607 if not authenticator.has_cached_credentials():
2608 raise auth.LoginRequiredError(cl.GetRietveldServer())
2609
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002610 # Apply watchlists on upload.
2611 change = cl.GetChange(base_branch, None)
2612 watchlist = watchlists.Watchlists(change.RepositoryRoot())
2613 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002614 if not options.bypass_watchlists:
2615 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002616
ukai@chromium.orge8077812012-02-03 03:41:46 +00002617 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00002618 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002619 # Set the reviewer list now so that presubmit checks can access it.
2620 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00002621 change_description.update_reviewers(options.reviewers,
2622 options.tbr_owners,
2623 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002624 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002625 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00002626 may_prompt=not options.force,
2627 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002628 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002629 if not hook_results.should_continue():
2630 return 1
2631 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002632 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002633
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002634 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002635 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002636 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00002637 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002638 print ('The last upload made from this repository was patchset #%d but '
2639 'the most recent patchset on the server is #%d.'
2640 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00002641 print ('Uploading will still work, but if you\'ve uploaded to this issue '
2642 'from another machine or branch the patch you\'re uploading now '
2643 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002644 ask_for_data('About to upload; enter to confirm.')
2645
iannucci@chromium.org79540052012-10-19 23:15:26 +00002646 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002647 if settings.GetIsGerrit():
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002648 if options.squash and options.no_squash:
2649 DieWithError('Can only use one of --squash or --no-squash')
2650
2651 options.squash = ((settings.GetSquashGerritUploads() or options.squash) and
2652 not options.no_squash)
2653
tandrii@chromium.org1e67bb72016-02-11 12:15:49 +00002654 ret = GerritUpload(options, args, cl, change)
2655 else:
2656 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002657 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00002658 git_set_branch_value('last-upload-hash',
2659 RunGit(['rev-parse', 'HEAD']).strip())
rmistry@google.com5626a922015-02-26 14:03:30 +00002660 # Run post upload hooks, if specified.
2661 if settings.GetRunPostUploadHook():
2662 presubmit_support.DoPostUploadExecuter(
2663 change,
2664 cl,
2665 settings.GetRoot(),
2666 options.verbose,
2667 sys.stdout)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002668
rmistry@google.com2dd99862015-06-22 12:22:18 +00002669 # Upload all dependencies if specified.
2670 if options.dependencies:
2671 print
2672 print '--dependencies has been specified.'
2673 print 'All dependent local branches will be re-uploaded.'
2674 print
2675 # Remove the dependencies flag from args so that we do not end up in a
2676 # loop.
2677 orig_args.remove('--dependencies')
2678 upload_branch_deps(cl, orig_args)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002679 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00002680
2681
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002682def IsSubmoduleMergeCommit(ref):
2683 # When submodules are added to the repo, we expect there to be a single
2684 # non-git-svn merge commit at remote HEAD with a signature comment.
2685 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00002686 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002687 return RunGit(cmd) != ''
2688
2689
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002690def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002691 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002692
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002693 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002694 Updates changelog with metadata (e.g. pointer to review).
2695 Pushes/dcommits the code upstream.
2696 Updates review and closes.
2697 """
2698 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2699 help='bypass upload presubmit hook')
2700 parser.add_option('-m', dest='message',
2701 help="override review description")
2702 parser.add_option('-f', action='store_true', dest='force',
2703 help="force yes to questions (don't prompt)")
2704 parser.add_option('-c', dest='contributor',
2705 help="external contributor for patch (appended to " +
2706 "description and used as author for git). Should be " +
2707 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002708 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002709 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002710 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002711 auth_config = auth.extract_auth_config_from_options(options)
2712
2713 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002714
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002715 current = cl.GetBranch()
2716 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2717 if not settings.GetIsGitSvn() and remote == '.':
2718 print
2719 print 'Attempting to push branch %r into another local branch!' % current
2720 print
2721 print 'Either reparent this branch on top of origin/master:'
2722 print ' git reparent-branch --root'
2723 print
2724 print 'OR run `git rebase-update` if you think the parent branch is already'
2725 print 'committed.'
2726 print
2727 print ' Current parent: %r' % upstream_branch
2728 return 1
2729
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002730 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002731 # Default to merging against our best guess of the upstream branch.
2732 args = [cl.GetUpstreamBranch()]
2733
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002734 if options.contributor:
2735 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
2736 print "Please provide contibutor as 'First Last <email@example.com>'"
2737 return 1
2738
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002739 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002740 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002741
sbc@chromium.org71437c02015-04-09 19:29:40 +00002742 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002743 return 1
2744
2745 # This rev-list syntax means "show all commits not in my branch that
2746 # are in base_branch".
2747 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
2748 base_branch]).splitlines()
2749 if upstream_commits:
2750 print ('Base branch "%s" has %d commits '
2751 'not in this branch.' % (base_branch, len(upstream_commits)))
2752 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
2753 return 1
2754
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002755 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002756 svn_head = None
2757 if cmd == 'dcommit' or base_has_submodules:
2758 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
2759 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002760
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002761 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002762 # If the base_head is a submodule merge commit, the first parent of the
2763 # base_head should be a git-svn commit, which is what we're interested in.
2764 base_svn_head = base_branch
2765 if base_has_submodules:
2766 base_svn_head += '^1'
2767
2768 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002769 if extra_commits:
2770 print ('This branch has %d additional commits not upstreamed yet.'
2771 % len(extra_commits.splitlines()))
2772 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
2773 'before attempting to %s.' % (base_branch, cmd))
2774 return 1
2775
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002776 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002777 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002778 author = None
2779 if options.contributor:
2780 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002781 hook_results = cl.RunHook(
2782 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002783 may_prompt=not options.force,
2784 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002785 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002786 if not hook_results.should_continue():
2787 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002788
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002789 # Check the tree status if the tree status URL is set.
2790 status = GetTreeStatus()
2791 if 'closed' == status:
2792 print('The tree is closed. Please wait for it to reopen. Use '
2793 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2794 return 1
2795 elif 'unknown' == status:
2796 print('Unable to determine tree status. Please verify manually and '
2797 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2798 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002799
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002800 change_desc = ChangeDescription(options.message)
2801 if not change_desc.description and cl.GetIssue():
2802 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002803
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002804 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00002805 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002806 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00002807 else:
2808 print 'No description set.'
2809 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
2810 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002811
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002812 # Keep a separate copy for the commit message, because the commit message
2813 # contains the link to the Rietveld issue, while the Rietveld message contains
2814 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002815 # Keep a separate copy for the commit message.
2816 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00002817 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002818
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002819 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00002820 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00002821 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00002822 # after it. Add a period on a new line to circumvent this. Also add a space
2823 # before the period to make sure that Gitiles continues to correctly resolve
2824 # the URL.
2825 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002826 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002827 commit_desc.append_footer('Patch from %s.' % options.contributor)
2828
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00002829 print('Description:')
2830 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002831
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002832 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002833 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00002834 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002835
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002836 # We want to squash all this branch's commits into one commit with the proper
2837 # description. We do this by doing a "reset --soft" to the base branch (which
2838 # keeps the working copy the same), then dcommitting that. If origin/master
2839 # has a submodule merge commit, we'll also need to cherry-pick the squashed
2840 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002841 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002842 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
2843 # Delete the branches if they exist.
2844 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
2845 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
2846 result = RunGitWithCode(showref_cmd)
2847 if result[0] == 0:
2848 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002849
2850 # We might be in a directory that's present in this branch but not in the
2851 # trunk. Move up to the top of the tree so that git commands that expect a
2852 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002853 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002854 if rel_base_path:
2855 os.chdir(rel_base_path)
2856
2857 # Stuff our change into the merge branch.
2858 # We wrap in a try...finally block so if anything goes wrong,
2859 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002860 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002861 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002862 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002863 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002864 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00002865 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002866 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002867 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002868 RunGit(
2869 [
2870 'commit', '--author', options.contributor,
2871 '-m', commit_desc.description,
2872 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002873 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002874 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002875 if base_has_submodules:
2876 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
2877 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
2878 RunGit(['checkout', CHERRY_PICK_BRANCH])
2879 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002880 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002881 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00002882 mirror = settings.GetGitMirror(remote)
2883 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002884 pending_prefix = settings.GetPendingRefPrefix()
2885 if not pending_prefix or branch.startswith(pending_prefix):
2886 # If not using refs/pending/heads/* at all, or target ref is already set
2887 # to pending, then push to the target ref directly.
2888 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00002889 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002890 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002891 else:
2892 # Cherry-pick the change on top of pending ref and then push it.
2893 assert branch.startswith('refs/'), branch
2894 assert pending_prefix[-1] == '/', pending_prefix
2895 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00002896 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002897 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002898 if retcode == 0:
2899 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002900 else:
2901 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002902 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002903 'svn', 'dcommit',
2904 '-C%s' % options.similarity,
2905 '--no-rebase', '--rmdir',
2906 ]
2907 if settings.GetForceHttpsCommitUrl():
2908 # Allow forcing https commit URLs for some projects that don't allow
2909 # committing to http URLs (like Google Code).
2910 remote_url = cl.GetGitSvnRemoteUrl()
2911 if urlparse.urlparse(remote_url).scheme == 'http':
2912 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002913 cmd_args.append('--commit-url=%s' % remote_url)
2914 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002915 if 'Committed r' in output:
2916 revision = re.match(
2917 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2918 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002919 finally:
2920 # And then swap back to the original branch and clean up.
2921 RunGit(['checkout', '-q', cl.GetBranch()])
2922 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002923 if base_has_submodules:
2924 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002925
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002926 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002927 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002928 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002929
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002930 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002931 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002932 try:
2933 revision = WaitForRealCommit(remote, revision, base_branch, branch)
2934 # We set pushed_to_pending to False, since it made it all the way to the
2935 # real ref.
2936 pushed_to_pending = False
2937 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002938 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002939
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002940 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002941 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002942 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002943 if not to_pending:
2944 if viewvc_url and revision:
2945 change_desc.append_footer(
2946 'Committed: %s%s' % (viewvc_url, revision))
2947 elif revision:
2948 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002949 print ('Closing issue '
2950 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002951 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002952 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002953 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002954 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00002955 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00002956 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002957 if options.bypass_hooks:
2958 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2959 else:
2960 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002961 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002962 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002963
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002964 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002965 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
2966 print 'The commit is in the pending queue (%s).' % pending_ref
2967 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00002968 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002969 'footer.' % branch)
2970
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002971 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2972 if os.path.isfile(hook):
2973 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002974
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002975 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002976
2977
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002978def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
2979 print
2980 print 'Waiting for commit to be landed on %s...' % real_ref
2981 print '(If you are impatient, you may Ctrl-C once without harm)'
2982 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
2983 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00002984 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002985
2986 loop = 0
2987 while True:
2988 sys.stdout.write('fetching (%d)... \r' % loop)
2989 sys.stdout.flush()
2990 loop += 1
2991
szager@chromium.org151ebcf2016-03-09 01:08:25 +00002992 if mirror:
2993 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002994 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
2995 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2996 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
2997 for commit in commits.splitlines():
2998 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
2999 print 'Found commit on %s' % real_ref
3000 return commit
3001
3002 current_rev = to_rev
3003
3004
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003005def PushToGitPending(remote, pending_ref, upstream_ref):
3006 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
3007
3008 Returns:
3009 (retcode of last operation, output log of last operation).
3010 """
3011 assert pending_ref.startswith('refs/'), pending_ref
3012 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
3013 cherry = RunGit(['rev-parse', 'HEAD']).strip()
3014 code = 0
3015 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003016 max_attempts = 3
3017 attempts_left = max_attempts
3018 while attempts_left:
3019 if attempts_left != max_attempts:
3020 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
3021 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003022
3023 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003024 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003025 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003026 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003027 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003028 print 'Fetch failed with exit code %d.' % code
3029 if out.strip():
3030 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003031 continue
3032
3033 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003034 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003035 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003036 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003037 if code:
3038 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003039 'Your patch doesn\'t apply cleanly to ref \'%s\', '
3040 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003041 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
3042 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003043 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003044 return code, out
3045
3046 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003047 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003048 code, out = RunGitWithCode(
3049 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
3050 if code == 0:
3051 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003052 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003053 return code, out
3054
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003055 print 'Push failed with exit code %d.' % code
3056 if out.strip():
3057 print out.strip()
3058 if IsFatalPushFailure(out):
3059 print (
3060 'Fatal push error. Make sure your .netrc credentials and git '
3061 'user.email are correct and you have push access to the repo.')
3062 return code, out
3063
3064 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003065 return code, out
3066
3067
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003068def IsFatalPushFailure(push_stdout):
3069 """True if retrying push won't help."""
3070 return '(prohibited by Gerrit)' in push_stdout
3071
3072
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003073@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003074def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003075 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003076 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003077 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003078 # If it looks like previous commits were mirrored with git-svn.
3079 message = """This repository appears to be a git-svn mirror, but no
3080upstream SVN master is set. You probably need to run 'git auto-svn' once."""
3081 else:
3082 message = """This doesn't appear to be an SVN repository.
3083If your project has a true, writeable git repository, you probably want to run
3084'git cl land' instead.
3085If your project has a git mirror of an upstream SVN master, you probably need
3086to run 'git svn init'.
3087
3088Using the wrong command might cause your commit to appear to succeed, and the
3089review to be closed, without actually landing upstream. If you choose to
3090proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00003091 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00003092 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003093 return SendUpstream(parser, args, 'dcommit')
3094
3095
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003096@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003097def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003098 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003099 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003100 print('This appears to be an SVN repository.')
3101 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003102 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00003103 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003104 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003105
3106
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003107def ParseIssueNum(arg):
3108 """Parses the issue number from args if present otherwise returns None."""
3109 if re.match(r'\d+', arg):
3110 return arg
3111 if arg.startswith('http'):
3112 return re.sub(r'.*/(\d+)/?', r'\1', arg)
3113 return None
3114
3115
3116@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003117def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00003118 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003119 parser.add_option('-b', dest='newbranch',
3120 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003121 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003122 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003123 parser.add_option('-d', '--directory', action='store', metavar='DIR',
3124 help='Change to the directory DIR immediately, '
3125 'before doing anything else.')
3126 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003127 help='failed patches spew .rej files rather than '
3128 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003129 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
3130 help="don't commit after patch applies")
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003131
3132 group = optparse.OptionGroup(parser,
3133 """Options for continuing work on the current issue uploaded
3134from a different clone (e.g. different machine). Must be used independently from
3135the other options. No issue number should be specified, and the branch must have
3136an issue number associated with it""")
3137 group.add_option('--reapply', action='store_true',
3138 dest='reapply',
3139 help="""Reset the branch and reapply the issue.
3140CAUTION: This will undo any local changes in this branch""")
3141
3142 group.add_option('--pull', action='store_true', dest='pull',
3143 help="Performs a pull before reapplying.")
3144 parser.add_option_group(group)
3145
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003146 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003147 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003148 auth_config = auth.extract_auth_config_from_options(options)
3149
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003150 issue_arg = None
3151 if options.reapply :
3152 if len(args) > 0:
3153 parser.error("--reapply implies no additional arguments.")
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003154
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003155 cl = Changelist()
3156 issue_arg = cl.GetIssue()
3157 upstream = cl.GetUpstreamBranch()
3158 if upstream == None:
3159 parser.error("No upstream branch specified. Cannot reset branch")
3160
3161 RunGit(['reset', '--hard', upstream])
3162 if options.pull:
3163 RunGit(['pull'])
3164 else:
3165 if len(args) != 1:
3166 parser.error("Must specify issue number")
3167
3168 issue_arg = ParseIssueNum(args[0])
3169
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003170 # The patch URL works because ParseIssueNum won't do any substitution
3171 # as the re.sub pattern fails to match and just returns it.
3172 if issue_arg == None:
3173 parser.print_help()
3174 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003175
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003176 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003177 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003178 return 1
3179
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003180 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00003181 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003182
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003183 if options.newbranch:
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003184 if options.reapply:
3185 parser.error("--reapply excludes any option other than --pull")
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003186 if options.force:
3187 RunGit(['branch', '-D', options.newbranch],
3188 stderr=subprocess2.PIPE, error_ok=True)
3189 RunGit(['checkout', '-b', options.newbranch,
3190 Changelist().GetUpstreamBranch()])
3191
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003192 return PatchIssue(issue_arg, options.reject, options.nocommit,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003193 options.directory, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003194
3195
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003196def PatchIssue(issue_arg, reject, nocommit, directory, auth_config):
wychen@chromium.orga872e752015-04-28 23:42:18 +00003197 # PatchIssue should never be called with a dirty tree. It is up to the
3198 # caller to check this, but just in case we assert here since the
3199 # consequences of the caller not checking this could be dire.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003200 assert(not git_common.is_dirty_git_tree('apply'))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003201
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003202 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003203 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00003204 issue = int(issue_arg)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003205 cl = Changelist(issue=issue, auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003206 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00003207 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003208 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00003209 # Assume it's a URL to the patch. Default to https.
3210 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
kjellander@chromium.org44424542015-06-02 18:35:29 +00003211 match = re.match(r'(.*?)/download/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00003212 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003213 DieWithError('Must pass an issue ID or full URL for '
3214 '\'Download raw patch set\'')
kjellander@chromium.org44424542015-06-02 18:35:29 +00003215 issue = int(match.group(2))
3216 cl = Changelist(issue=issue, auth_config=auth_config)
3217 cl.rietveld_server = match.group(1)
3218 patchset = int(match.group(3))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00003219 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003220
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003221 # Switch up to the top-level directory, if necessary, in preparation for
3222 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003223 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003224 if top:
3225 os.chdir(top)
3226
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003227 # Git patches have a/ at the beginning of source paths. We strip that out
3228 # with a sed script rather than the -p flag to patch so we can feed either
3229 # Git or svn-style patches into the same apply command.
3230 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003231 try:
3232 patch_data = subprocess2.check_output(
3233 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
3234 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003235 DieWithError('Git patch mungling failed.')
3236 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003237
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003238 # We use "git apply" to apply the patch instead of "patch" so that we can
3239 # pick up file adds.
3240 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003241 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003242 if directory:
3243 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003244 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003245 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003246 elif IsGitVersionAtLeast('1.7.12'):
3247 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003248 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003249 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003250 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003251 except subprocess2.CalledProcessError:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003252 print 'Failed to apply the patch'
3253 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003254
3255 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003256 if not nocommit:
wychen@chromium.org5b3bebb2015-05-28 21:41:43 +00003257 RunGit(['commit', '-m', (cl.GetDescription() + '\n\n' +
3258 'patch from issue %(i)s at patchset '
carlosk@chromium.org71284d92014-11-14 18:12:50 +00003259 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
3260 % {'i': issue, 'p': patchset})])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003261 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003262 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00003263 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00003264 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003265 else:
3266 print "Patch applied to index."
3267 return 0
3268
3269
3270def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003271 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003272 # Provide a wrapper for git svn rebase to help avoid accidental
3273 # git svn dcommit.
3274 # It's the only command that doesn't use parser at all since we just defer
3275 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003276
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003277 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003278
3279
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003280def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003281 """Fetches the tree status and returns either 'open', 'closed',
3282 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003283 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003284 if url:
3285 status = urllib2.urlopen(url).read().lower()
3286 if status.find('closed') != -1 or status == '0':
3287 return 'closed'
3288 elif status.find('open') != -1 or status == '1':
3289 return 'open'
3290 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003291 return 'unset'
3292
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003293
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003294def GetTreeStatusReason():
3295 """Fetches the tree status from a json url and returns the message
3296 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00003297 url = settings.GetTreeStatusUrl()
3298 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003299 connection = urllib2.urlopen(json_url)
3300 status = json.loads(connection.read())
3301 connection.close()
3302 return status['message']
3303
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003304
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003305def GetBuilderMaster(bot_list):
3306 """For a given builder, fetch the master from AE if available."""
3307 map_url = 'https://builders-map.appspot.com/'
3308 try:
3309 master_map = json.load(urllib2.urlopen(map_url))
3310 except urllib2.URLError as e:
3311 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
3312 (map_url, e))
3313 except ValueError as e:
3314 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
3315 if not master_map:
3316 return None, 'Failed to build master map.'
3317
3318 result_master = ''
3319 for bot in bot_list:
3320 builder = bot.split(':', 1)[0]
3321 master_list = master_map.get(builder, [])
3322 if not master_list:
3323 return None, ('No matching master for builder %s.' % builder)
3324 elif len(master_list) > 1:
3325 return None, ('The builder name %s exists in multiple masters %s.' %
3326 (builder, master_list))
3327 else:
3328 cur_master = master_list[0]
3329 if not result_master:
3330 result_master = cur_master
3331 elif result_master != cur_master:
3332 return None, 'The builders do not belong to the same master.'
3333 return result_master, None
3334
3335
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003336def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003337 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003338 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003339 status = GetTreeStatus()
3340 if 'unset' == status:
3341 print 'You must configure your tree status URL by running "git cl config".'
3342 return 2
3343
3344 print "The tree is %s" % status
3345 print
3346 print GetTreeStatusReason()
3347 if status != 'open':
3348 return 1
3349 return 0
3350
3351
maruel@chromium.org15192402012-09-06 12:38:29 +00003352def CMDtry(parser, args):
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003353 """Triggers a try job through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00003354 group = optparse.OptionGroup(parser, "Try job options")
3355 group.add_option(
3356 "-b", "--bot", action="append",
3357 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
3358 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003359 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00003360 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003361 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00003362 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003363 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00003364 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003365 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003366 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003367 "-r", "--revision",
3368 help="Revision to use for the try job; default: the "
3369 "revision will be determined by the try server; see "
3370 "its waterfall for more info")
3371 group.add_option(
3372 "-c", "--clobber", action="store_true", default=False,
3373 help="Force a clobber before building; e.g. don't do an "
3374 "incremental build")
3375 group.add_option(
3376 "--project",
3377 help="Override which project to use. Projects are defined "
3378 "server-side to define what default bot set to use")
3379 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00003380 "-p", "--property", dest="properties", action="append", default=[],
3381 help="Specify generic properties in the form -p key1=value1 -p "
3382 "key2=value2 etc (buildbucket only). The value will be treated as "
3383 "json if decodable, or as string otherwise.")
3384 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003385 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003386 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003387 "--use-rietveld", action="store_true", default=False,
3388 help="Use Rietveld to trigger try jobs.")
3389 group.add_option(
3390 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3391 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00003392 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003393 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00003394 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003395 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00003396
machenbach@chromium.org45453142015-09-15 08:45:22 +00003397 if options.use_rietveld and options.properties:
3398 parser.error('Properties can only be specified with buildbucket')
3399
3400 # Make sure that all properties are prop=value pairs.
3401 bad_params = [x for x in options.properties if '=' not in x]
3402 if bad_params:
3403 parser.error('Got properties with missing "=": %s' % bad_params)
3404
maruel@chromium.org15192402012-09-06 12:38:29 +00003405 if args:
3406 parser.error('Unknown arguments: %s' % args)
3407
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003408 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00003409 if not cl.GetIssue():
3410 parser.error('Need to upload first')
3411
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003412 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00003413 if props.get('closed'):
3414 parser.error('Cannot send tryjobs for a closed CL')
3415
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003416 if props.get('private'):
3417 parser.error('Cannot use trybots with private issue')
3418
maruel@chromium.org15192402012-09-06 12:38:29 +00003419 if not options.name:
3420 options.name = cl.GetBranch()
3421
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003422 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003423 options.master, err_msg = GetBuilderMaster(options.bot)
3424 if err_msg:
3425 parser.error('Tryserver master cannot be found because: %s\n'
3426 'Please manually specify the tryserver master'
3427 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003428
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003429 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003430 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003431 if not options.bot:
3432 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00003433
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003434 # Get try masters from PRESUBMIT.py files.
3435 masters = presubmit_support.DoGetTryMasters(
3436 change,
3437 change.LocalPaths(),
3438 settings.GetRoot(),
3439 None,
3440 None,
3441 options.verbose,
3442 sys.stdout)
3443 if masters:
3444 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00003445
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003446 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
3447 options.bot = presubmit_support.DoGetTrySlaves(
3448 change,
3449 change.LocalPaths(),
3450 settings.GetRoot(),
3451 None,
3452 None,
3453 options.verbose,
3454 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00003455
3456 if not options.bot:
3457 # Get try masters from cq.cfg if any.
3458 # TODO(tandrii): some (but very few) projects store cq.cfg in different
3459 # location.
3460 cq_cfg = os.path.join(change.RepositoryRoot(),
3461 'infra', 'config', 'cq.cfg')
3462 if os.path.exists(cq_cfg):
3463 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00003464 cq_masters = commit_queue.get_master_builder_map(
3465 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00003466 for master, builders in cq_masters.iteritems():
3467 for builder in builders:
3468 # Skip presubmit builders, because these will fail without LGTM.
3469 if 'presubmit' not in builder.lower():
3470 masters.setdefault(master, {})[builder] = ['defaulttests']
3471 if masters:
3472 return masters
3473
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003474 if not options.bot:
3475 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00003476
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003477 builders_and_tests = {}
3478 # TODO(machenbach): The old style command-line options don't support
3479 # multiple try masters yet.
3480 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
3481 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
3482
3483 for bot in old_style:
3484 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003485 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003486 elif ',' in bot:
3487 parser.error('Specify one bot per --bot flag')
3488 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00003489 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003490
3491 for bot, tests in new_style:
3492 builders_and_tests.setdefault(bot, []).extend(tests)
3493
3494 # Return a master map with one master to be backwards compatible. The
3495 # master name defaults to an empty string, which will cause the master
3496 # not to be set on rietveld (deprecated).
3497 return {options.master: builders_and_tests}
3498
3499 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00003500
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003501 for builders in masters.itervalues():
3502 if any('triggered' in b for b in builders):
3503 print >> sys.stderr, (
3504 'ERROR You are trying to send a job to a triggered bot. This type of'
3505 ' bot requires an\ninitial job from a parent (usually a builder). '
3506 'Instead send your job to the parent.\n'
3507 'Bot list: %s' % builders)
3508 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00003509
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00003510 patchset = cl.GetMostRecentPatchset()
3511 if patchset and patchset != cl.GetPatchset():
3512 print(
3513 '\nWARNING Mismatch between local config and server. Did a previous '
3514 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3515 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003516 if options.luci:
3517 trigger_luci_job(cl, masters, options)
3518 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003519 try:
3520 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
3521 except BuildbucketResponseException as ex:
3522 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00003523 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003524 except Exception as e:
3525 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3526 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
3527 e, stacktrace)
3528 return 1
3529 else:
3530 try:
3531 cl.RpcServer().trigger_distributed_try_jobs(
3532 cl.GetIssue(), patchset, options.name, options.clobber,
3533 options.revision, masters)
3534 except urllib2.HTTPError as e:
3535 if e.code == 404:
3536 print('404 from rietveld; '
3537 'did you mean to use "git try" instead of "git cl try"?')
3538 return 1
3539 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003540
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003541 for (master, builders) in sorted(masters.iteritems()):
3542 if master:
3543 print 'Master: %s' % master
3544 length = max(len(builder) for builder in builders)
3545 for builder in sorted(builders):
3546 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00003547 return 0
3548
3549
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00003550def CMDtry_results(parser, args):
3551 group = optparse.OptionGroup(parser, "Try job results options")
3552 group.add_option(
3553 "-p", "--patchset", type=int, help="patchset number if not current.")
3554 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00003555 "--print-master", action='store_true', help="print master name as well.")
3556 group.add_option(
3557 "--color", action='store_true', default=sys.stdout.isatty(),
3558 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00003559 group.add_option(
3560 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3561 help="Host of buildbucket. The default host is %default.")
3562 parser.add_option_group(group)
3563 auth.add_auth_options(parser)
3564 options, args = parser.parse_args(args)
3565 if args:
3566 parser.error('Unrecognized args: %s' % ' '.join(args))
3567
3568 auth_config = auth.extract_auth_config_from_options(options)
3569 cl = Changelist(auth_config=auth_config)
3570 if not cl.GetIssue():
3571 parser.error('Need to upload first')
3572
3573 if not options.patchset:
3574 options.patchset = cl.GetMostRecentPatchset()
3575 if options.patchset and options.patchset != cl.GetPatchset():
3576 print(
3577 '\nWARNING Mismatch between local config and server. Did a previous '
3578 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3579 'Continuing using\npatchset %s.\n' % options.patchset)
3580 try:
3581 jobs = fetch_try_jobs(auth_config, cl, options)
3582 except BuildbucketResponseException as ex:
3583 print 'Buildbucket error: %s' % ex
3584 return 1
3585 except Exception as e:
3586 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3587 print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
3588 e, stacktrace)
3589 return 1
3590 print_tryjobs(options, jobs)
3591 return 0
3592
3593
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003594@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003595def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003596 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003597 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003598 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003599 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003600
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003601 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003602 if args:
3603 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003604 branch = cl.GetBranch()
3605 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003606 cl = Changelist()
3607 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003608
3609 # Clear configured merge-base, if there is one.
3610 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003611 else:
3612 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003613 return 0
3614
3615
thestig@chromium.org00858c82013-12-02 23:08:03 +00003616def CMDweb(parser, args):
3617 """Opens the current CL in the web browser."""
3618 _, args = parser.parse_args(args)
3619 if args:
3620 parser.error('Unrecognized args: %s' % ' '.join(args))
3621
3622 issue_url = Changelist().GetIssueURL()
3623 if not issue_url:
3624 print >> sys.stderr, 'ERROR No issue to open'
3625 return 1
3626
3627 webbrowser.open(issue_url)
3628 return 0
3629
3630
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003631def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003632 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003633 auth.add_auth_options(parser)
3634 options, args = parser.parse_args(args)
3635 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003636 if args:
3637 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003638 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003639 props = cl.GetIssueProperties()
3640 if props.get('private'):
3641 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003642 cl.SetFlag('commit', '1')
3643 return 0
3644
3645
groby@chromium.org411034a2013-02-26 15:12:01 +00003646def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003647 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003648 auth.add_auth_options(parser)
3649 options, args = parser.parse_args(args)
3650 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00003651 if args:
3652 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003653 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00003654 # Ensure there actually is an issue to close.
3655 cl.GetDescription()
3656 cl.CloseIssue()
3657 return 0
3658
3659
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003660def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003661 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003662 auth.add_auth_options(parser)
3663 options, args = parser.parse_args(args)
3664 auth_config = auth.extract_auth_config_from_options(options)
3665 if args:
3666 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003667
3668 # Uncommitted (staged and unstaged) changes will be destroyed by
3669 # "git reset --hard" if there are merging conflicts in PatchIssue().
3670 # Staged changes would be committed along with the patch from last
3671 # upload, hence counted toward the "last upload" side in the final
3672 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003673 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003674 return 1
3675
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003676 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003677 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003678 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003679 if not issue:
3680 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003681 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003682 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003683
3684 # Create a new branch based on the merge-base
3685 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
3686 try:
3687 # Patch in the latest changes from rietveld.
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003688 rtn = PatchIssue(issue, False, False, None, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003689 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003690 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003691 return rtn
3692
wychen@chromium.org06928532015-02-03 02:11:29 +00003693 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003694 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00003695 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003696 finally:
3697 RunGit(['checkout', '-q', branch])
3698 RunGit(['branch', '-D', TMP_BRANCH])
3699
3700 return 0
3701
3702
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003703def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003704 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003705 parser.add_option(
3706 '--no-color',
3707 action='store_true',
3708 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003709 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003710 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003711 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003712
3713 author = RunGit(['config', 'user.email']).strip() or None
3714
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003715 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003716
3717 if args:
3718 if len(args) > 1:
3719 parser.error('Unknown args')
3720 base_branch = args[0]
3721 else:
3722 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003723 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003724
3725 change = cl.GetChange(base_branch, None)
3726 return owners_finder.OwnersFinder(
3727 [f.LocalPath() for f in
3728 cl.GetChange(base_branch, None).AffectedFiles()],
3729 change.RepositoryRoot(), author,
3730 fopen=file, os_path=os.path, glob=glob.glob,
3731 disable_color=options.no_color).run()
3732
3733
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003734def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003735 """Generates a diff command."""
3736 # Generate diff for the current branch's changes.
3737 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
3738 upstream_commit, '--' ]
3739
3740 if args:
3741 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003742 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003743 diff_cmd.append(arg)
3744 else:
3745 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003746
3747 return diff_cmd
3748
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003749def MatchingFileType(file_name, extensions):
3750 """Returns true if the file name ends with one of the given extensions."""
3751 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003752
enne@chromium.org555cfe42014-01-29 18:21:39 +00003753@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003754def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003755 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00003756 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00003757 GN_EXTS = ['.gn', '.gni']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003758 parser.add_option('--full', action='store_true',
3759 help='Reformat the full content of all touched files')
3760 parser.add_option('--dry-run', action='store_true',
3761 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003762 parser.add_option('--python', action='store_true',
3763 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003764 parser.add_option('--diff', action='store_true',
3765 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003766 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003767
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003768 # git diff generates paths against the root of the repository. Change
3769 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003770 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003771 if rel_base_path:
3772 os.chdir(rel_base_path)
3773
digit@chromium.org29e47272013-05-17 17:01:46 +00003774 # Grab the merge-base commit, i.e. the upstream commit of the current
3775 # branch when it was created or the last time it was rebased. This is
3776 # to cover the case where the user may have called "git fetch origin",
3777 # moving the origin branch to a newer commit, but hasn't rebased yet.
3778 upstream_commit = None
3779 cl = Changelist()
3780 upstream_branch = cl.GetUpstreamBranch()
3781 if upstream_branch:
3782 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
3783 upstream_commit = upstream_commit.strip()
3784
3785 if not upstream_commit:
3786 DieWithError('Could not find base commit for this branch. '
3787 'Are you in detached state?')
3788
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003789 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
3790 diff_output = RunGit(changed_files_cmd)
3791 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00003792 # Filter out files deleted by this CL
3793 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003794
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003795 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
3796 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
3797 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00003798 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00003799
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003800 top_dir = os.path.normpath(
3801 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
3802
3803 # Locate the clang-format binary in the checkout
3804 try:
3805 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
3806 except clang_format.NotFoundError, e:
3807 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00003808
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003809 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
3810 # formatted. This is used to block during the presubmit.
3811 return_value = 0
3812
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003813 if clang_diff_files:
3814 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003815 cmd = [clang_format_tool]
3816 if not opts.dry_run and not opts.diff:
3817 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003818 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003819 if opts.diff:
3820 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003821 else:
3822 env = os.environ.copy()
3823 env['PATH'] = str(os.path.dirname(clang_format_tool))
3824 try:
3825 script = clang_format.FindClangFormatScriptInChromiumTree(
3826 'clang-format-diff.py')
3827 except clang_format.NotFoundError, e:
3828 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003829
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003830 cmd = [sys.executable, script, '-p0']
3831 if not opts.dry_run and not opts.diff:
3832 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003833
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003834 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
3835 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003836
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003837 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
3838 if opts.diff:
3839 sys.stdout.write(stdout)
3840 if opts.dry_run and len(stdout) > 0:
3841 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003842
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003843 # Similar code to above, but using yapf on .py files rather than clang-format
3844 # on C/C++ files
3845 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003846 yapf_tool = gclient_utils.FindExecutable('yapf')
3847 if yapf_tool is None:
3848 DieWithError('yapf not found in PATH')
3849
3850 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003851 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003852 cmd = [yapf_tool]
3853 if not opts.dry_run and not opts.diff:
3854 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003855 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003856 if opts.diff:
3857 sys.stdout.write(stdout)
3858 else:
3859 # TODO(sbc): yapf --lines mode still has some issues.
3860 # https://github.com/google/yapf/issues/154
3861 DieWithError('--python currently only works with --full')
3862
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003863 # Dart's formatter does not have the nice property of only operating on
3864 # modified chunks, so hard code full.
3865 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003866 try:
3867 command = [dart_format.FindDartFmtToolInChromiumTree()]
3868 if not opts.dry_run and not opts.diff:
3869 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003870 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003871
ppi@chromium.org6593d932016-03-03 15:41:15 +00003872 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003873 if opts.dry_run and stdout:
3874 return_value = 2
3875 except dart_format.NotFoundError as e:
erikcorry@chromium.org3e445022015-12-17 09:07:26 +00003876 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
3877 'found in this checkout. Files in other languages are still ' +
3878 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003879
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00003880 # Format GN build files. Always run on full build files for canonical form.
3881 if gn_diff_files:
3882 cmd = ['gn', 'format']
3883 if not opts.dry_run and not opts.diff:
3884 cmd.append('--in-place')
3885 for gn_diff_file in gn_diff_files:
3886 stdout = RunCommand(cmd + [gn_diff_file], cwd=top_dir)
3887 if opts.diff:
3888 sys.stdout.write(stdout)
3889
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003890 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003891
3892
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00003893@subcommand.usage('<codereview url or issue id>')
3894def CMDcheckout(parser, args):
3895 """Checks out a branch associated with a given Rietveld issue."""
3896 _, args = parser.parse_args(args)
3897
3898 if len(args) != 1:
3899 parser.print_help()
3900 return 1
3901
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003902 target_issue = ParseIssueNum(args[0])
3903 if target_issue == None:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00003904 parser.print_help()
3905 return 1
3906
3907 key_and_issues = [x.split() for x in RunGit(
3908 ['config', '--local', '--get-regexp', r'branch\..*\.rietveldissue'])
3909 .splitlines()]
3910 branches = []
3911 for key, issue in key_and_issues:
3912 if issue == target_issue:
3913 branches.append(re.sub(r'branch\.(.*)\.rietveldissue', r'\1', key))
3914
3915 if len(branches) == 0:
3916 print 'No branch found for issue %s.' % target_issue
3917 return 1
3918 if len(branches) == 1:
3919 RunGit(['checkout', branches[0]])
3920 else:
3921 print 'Multiple branches match issue %s:' % target_issue
3922 for i in range(len(branches)):
3923 print '%d: %s' % (i, branches[i])
3924 which = raw_input('Choose by index: ')
3925 try:
3926 RunGit(['checkout', branches[int(which)]])
3927 except (IndexError, ValueError):
3928 print 'Invalid selection, not checking out any branch.'
3929 return 1
3930
3931 return 0
3932
3933
maruel@chromium.org29404b52014-09-08 22:58:00 +00003934def CMDlol(parser, args):
3935 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00003936 print zlib.decompress(base64.b64decode(
3937 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
3938 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
3939 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
3940 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00003941 return 0
3942
3943
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003944class OptionParser(optparse.OptionParser):
3945 """Creates the option parse and add --verbose support."""
3946 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003947 optparse.OptionParser.__init__(
3948 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003949 self.add_option(
3950 '-v', '--verbose', action='count', default=0,
3951 help='Use 2 times for more debugging info')
3952
3953 def parse_args(self, args=None, values=None):
3954 options, args = optparse.OptionParser.parse_args(self, args, values)
3955 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
3956 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
3957 return options, args
3958
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003959
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003960def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00003961 if sys.hexversion < 0x02060000:
3962 print >> sys.stderr, (
3963 '\nYour python version %s is unsupported, please upgrade.\n' %
3964 sys.version.split(' ', 1)[0])
3965 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003966
maruel@chromium.orgddd59412011-11-30 14:20:38 +00003967 # Reload settings.
3968 global settings
3969 settings = Settings()
3970
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003971 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003972 dispatcher = subcommand.CommandDispatcher(__name__)
3973 try:
3974 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003975 except auth.AuthenticationError as e:
3976 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003977 except urllib2.HTTPError, e:
3978 if e.code != 500:
3979 raise
3980 DieWithError(
3981 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
3982 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00003983 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003984
3985
3986if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003987 # These affect sys.stdout so do it outside of main() to simplify mocks in
3988 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00003989 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003990 colorama.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00003991 try:
3992 sys.exit(main(sys.argv[1:]))
3993 except KeyboardInterrupt:
3994 sys.stderr.write('interrupted\n')
3995 sys.exit(1)