blob: fe03778ba87811e7c06027b6f9da47570deb8bdc [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
maruel@chromium.org725f1c32011-04-01 20:24:54 +00008"""A git-command for integrating reviews on Rietveld."""
9
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000010from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000011from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000012import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000013import collections
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000014import glob
sheyang@google.com6ebaf782015-05-12 19:17:54 +000015import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000016import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import logging
18import optparse
19import os
maruel@chromium.org1033efd2013-07-23 23:25:09 +000020import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000022import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000023import sys
bauerb@chromium.org27386dd2015-02-16 10:45:39 +000024import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025import textwrap
sheyang@google.com6ebaf782015-05-12 19:17:54 +000026import time
27import traceback
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000028import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000030import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000031import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000032import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000033import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000034
35try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000036 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000037except ImportError:
38 pass
39
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000040from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000041from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000043import auth
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +000044from luci_hacks import trigger_luci_job as luci_trigger
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000045import clang_format
tandrii@chromium.org71184c02016-01-13 15:18:44 +000046import commit_queue
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000047import dart_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000048import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000049import gclient_utils
iannucci@chromium.org9e849272014-04-04 00:31:55 +000050import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000051import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000052import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000053import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000054import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000055import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000056import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000057import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000058import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000059import watchlists
60
maruel@chromium.org0633fb42013-08-16 20:06:14 +000061__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000062
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000063DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000064POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000065DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000066GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000067CHANGE_ID = 'Change-Id:'
rmistry@google.comc68112d2015-03-03 12:48:06 +000068REFS_THAT_ALIAS_TO_OTHER_REFS = {
69 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
70 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
71}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000072
thestig@chromium.org44202a22014-03-11 19:22:18 +000073# Valid extensions for files we want to lint.
74DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
75DEFAULT_LINT_IGNORE_REGEX = r"$^"
76
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000077# Shortcut since it quickly becomes redundant.
78Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000079
maruel@chromium.orgddd59412011-11-30 14:20:38 +000080# Initialized in main()
81settings = None
82
83
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000084def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000085 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000086 sys.exit(1)
87
88
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000089def GetNoGitPagerEnv():
90 env = os.environ.copy()
91 # 'cat' is a magical git string that disables pagers on all platforms.
92 env['GIT_PAGER'] = 'cat'
93 return env
94
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000095
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000096def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000097 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000098 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000099 except subprocess2.CalledProcessError as e:
100 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000101 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000102 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000103 'Command "%s" failed.\n%s' % (
104 ' '.join(args), error_message or e.stdout or ''))
105 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000106
107
108def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000109 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000110 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000111
112
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000113def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000114 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000115 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000116 if suppress_stderr:
117 stderr = subprocess2.VOID
118 else:
119 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000120 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000121 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000122 stdout=subprocess2.PIPE,
123 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000124 return code, out[0]
125 except ValueError:
126 # When the subprocess fails, it returns None. That triggers a ValueError
127 # when trying to unpack the return value into (out, code).
128 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000129
130
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000131def RunGitSilent(args):
132 """Returns stdout, suppresses stderr and ingores the return code."""
133 return RunGitWithCode(args, suppress_stderr=True)[1]
134
135
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000136def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000137 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000138 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000139 return (version.startswith(prefix) and
140 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000141
142
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000143def BranchExists(branch):
144 """Return True if specified branch exists."""
145 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
146 suppress_stderr=True)
147 return not code
148
149
maruel@chromium.org90541732011-04-01 17:54:18 +0000150def ask_for_data(prompt):
151 try:
152 return raw_input(prompt)
153 except KeyboardInterrupt:
154 # Hide the exception.
155 sys.exit(1)
156
157
iannucci@chromium.org79540052012-10-19 23:15:26 +0000158def git_set_branch_value(key, value):
159 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000160 if not branch:
161 return
162
163 cmd = ['config']
164 if isinstance(value, int):
165 cmd.append('--int')
166 git_key = 'branch.%s.%s' % (branch, key)
167 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000168
169
170def git_get_branch_default(key, default):
171 branch = Changelist().GetBranch()
172 if branch:
173 git_key = 'branch.%s.%s' % (branch, key)
174 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
175 try:
176 return int(stdout.strip())
177 except ValueError:
178 pass
179 return default
180
181
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000182def add_git_similarity(parser):
183 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000184 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000185 help='Sets the percentage that a pair of files need to match in order to'
186 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000187 parser.add_option(
188 '--find-copies', action='store_true',
189 help='Allows git to look for copies.')
190 parser.add_option(
191 '--no-find-copies', action='store_false', dest='find_copies',
192 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000193
194 old_parser_args = parser.parse_args
195 def Parse(args):
196 options, args = old_parser_args(args)
197
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000198 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000199 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000200 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000201 print('Note: Saving similarity of %d%% in git config.'
202 % options.similarity)
203 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000204
iannucci@chromium.org79540052012-10-19 23:15:26 +0000205 options.similarity = max(0, min(options.similarity, 100))
206
207 if options.find_copies is None:
208 options.find_copies = bool(
209 git_get_branch_default('git-find-copies', True))
210 else:
211 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000212
213 print('Using %d%% similarity for rename/copy detection. '
214 'Override with --similarity.' % options.similarity)
215
216 return options, args
217 parser.parse_args = Parse
218
219
machenbach@chromium.org45453142015-09-15 08:45:22 +0000220def _get_properties_from_options(options):
221 properties = dict(x.split('=', 1) for x in options.properties)
222 for key, val in properties.iteritems():
223 try:
224 properties[key] = json.loads(val)
225 except ValueError:
226 pass # If a value couldn't be evaluated, treat it as a string.
227 return properties
228
229
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000230def _prefix_master(master):
231 """Convert user-specified master name to full master name.
232
233 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
234 name, while the developers always use shortened master name
235 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
236 function does the conversion for buildbucket migration.
237 """
238 prefix = 'master.'
239 if master.startswith(prefix):
240 return master
241 return '%s%s' % (prefix, master)
242
243
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000244def _buildbucket_retry(operation_name, http, *args, **kwargs):
245 """Retries requests to buildbucket service and returns parsed json content."""
246 try_count = 0
247 while True:
248 response, content = http.request(*args, **kwargs)
249 try:
250 content_json = json.loads(content)
251 except ValueError:
252 content_json = None
253
254 # Buildbucket could return an error even if status==200.
255 if content_json and content_json.get('error'):
256 msg = 'Error in response. Reason: %s. Message: %s.' % (
257 content_json['error'].get('reason', ''),
258 content_json['error'].get('message', ''))
259 raise BuildbucketResponseException(msg)
260
261 if response.status == 200:
262 if not content_json:
263 raise BuildbucketResponseException(
264 'Buildbucket returns invalid json content: %s.\n'
265 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
266 content)
267 return content_json
268 if response.status < 500 or try_count >= 2:
269 raise httplib2.HttpLib2Error(content)
270
271 # status >= 500 means transient failures.
272 logging.debug('Transient errors when %s. Will retry.', operation_name)
273 time.sleep(0.5 + 1.5*try_count)
274 try_count += 1
275 assert False, 'unreachable'
276
277
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000278def trigger_luci_job(changelist, masters, options):
279 """Send a job to run on LUCI."""
280 issue_props = changelist.GetIssueProperties()
281 issue = changelist.GetIssue()
282 patchset = changelist.GetMostRecentPatchset()
283 for builders_and_tests in sorted(masters.itervalues()):
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000284 # TODO(hinoka et al): add support for other properties.
285 # Currently, this completely ignores testfilter and other properties.
286 for builder in sorted(builders_and_tests):
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000287 luci_trigger.trigger(
288 builder, 'HEAD', issue, patchset, issue_props['project'])
289
290
machenbach@chromium.org45453142015-09-15 08:45:22 +0000291def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000292 rietveld_url = settings.GetDefaultServerUrl()
293 rietveld_host = urlparse.urlparse(rietveld_url).hostname
294 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
295 http = authenticator.authorize(httplib2.Http())
296 http.force_exception_to_status_code = True
297 issue_props = changelist.GetIssueProperties()
298 issue = changelist.GetIssue()
299 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000300 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000301
302 buildbucket_put_url = (
303 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000304 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000305 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
306 hostname=rietveld_host,
307 issue=issue,
308 patch=patchset)
309
310 batch_req_body = {'builds': []}
311 print_text = []
312 print_text.append('Tried jobs on:')
313 for master, builders_and_tests in sorted(masters.iteritems()):
314 print_text.append('Master: %s' % master)
315 bucket = _prefix_master(master)
316 for builder, tests in sorted(builders_and_tests.iteritems()):
317 print_text.append(' %s: %s' % (builder, tests))
318 parameters = {
319 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000320 'changes': [{
321 'author': {'email': issue_props['owner_email']},
322 'revision': options.revision,
323 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000324 'properties': {
325 'category': category,
326 'issue': issue,
327 'master': master,
328 'patch_project': issue_props['project'],
329 'patch_storage': 'rietveld',
330 'patchset': patchset,
331 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000332 'rietveld': rietveld_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000333 },
334 }
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000335 if tests:
336 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000337 if properties:
338 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000339 if options.clobber:
340 parameters['properties']['clobber'] = True
341 batch_req_body['builds'].append(
342 {
343 'bucket': bucket,
344 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000345 'client_operation_id': str(uuid.uuid4()),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000346 'tags': ['builder:%s' % builder,
347 'buildset:%s' % buildset,
348 'master:%s' % master,
349 'user_agent:git_cl_try']
350 }
351 )
352
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000353 _buildbucket_retry(
354 'triggering tryjobs',
355 http,
356 buildbucket_put_url,
357 'PUT',
358 body=json.dumps(batch_req_body),
359 headers={'Content-Type': 'application/json'}
360 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000361 print_text.append('To see results here, run: git cl try-results')
362 print_text.append('To see results in browser, run: git cl web')
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000363 print '\n'.join(print_text)
kjellander@chromium.org44424542015-06-02 18:35:29 +0000364
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000365
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000366def fetch_try_jobs(auth_config, changelist, options):
367 """Fetches tryjobs from buildbucket.
368
369 Returns a map from build id to build info as json dictionary.
370 """
371 rietveld_url = settings.GetDefaultServerUrl()
372 rietveld_host = urlparse.urlparse(rietveld_url).hostname
373 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
374 if authenticator.has_cached_credentials():
375 http = authenticator.authorize(httplib2.Http())
376 else:
377 print ('Warning: Some results might be missing because %s' %
378 # Get the message on how to login.
379 auth.LoginRequiredError(rietveld_host).message)
380 http = httplib2.Http()
381
382 http.force_exception_to_status_code = True
383
384 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
385 hostname=rietveld_host,
386 issue=changelist.GetIssue(),
387 patch=options.patchset)
388 params = {'tag': 'buildset:%s' % buildset}
389
390 builds = {}
391 while True:
392 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
393 hostname=options.buildbucket_host,
394 params=urllib.urlencode(params))
395 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
396 for build in content.get('builds', []):
397 builds[build['id']] = build
398 if 'next_cursor' in content:
399 params['start_cursor'] = content['next_cursor']
400 else:
401 break
402 return builds
403
404
405def print_tryjobs(options, builds):
406 """Prints nicely result of fetch_try_jobs."""
407 if not builds:
408 print 'No tryjobs scheduled'
409 return
410
411 # Make a copy, because we'll be modifying builds dictionary.
412 builds = builds.copy()
413 builder_names_cache = {}
414
415 def get_builder(b):
416 try:
417 return builder_names_cache[b['id']]
418 except KeyError:
419 try:
420 parameters = json.loads(b['parameters_json'])
421 name = parameters['builder_name']
422 except (ValueError, KeyError) as error:
423 print 'WARNING: failed to get builder name for build %s: %s' % (
424 b['id'], error)
425 name = None
426 builder_names_cache[b['id']] = name
427 return name
428
429 def get_bucket(b):
430 bucket = b['bucket']
431 if bucket.startswith('master.'):
432 return bucket[len('master.'):]
433 return bucket
434
435 if options.print_master:
436 name_fmt = '%%-%ds %%-%ds' % (
437 max(len(str(get_bucket(b))) for b in builds.itervalues()),
438 max(len(str(get_builder(b))) for b in builds.itervalues()))
439 def get_name(b):
440 return name_fmt % (get_bucket(b), get_builder(b))
441 else:
442 name_fmt = '%%-%ds' % (
443 max(len(str(get_builder(b))) for b in builds.itervalues()))
444 def get_name(b):
445 return name_fmt % get_builder(b)
446
447 def sort_key(b):
448 return b['status'], b.get('result'), get_name(b), b.get('url')
449
450 def pop(title, f, color=None, **kwargs):
451 """Pop matching builds from `builds` dict and print them."""
452
453 if not sys.stdout.isatty() or color is None:
454 colorize = str
455 else:
456 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
457
458 result = []
459 for b in builds.values():
460 if all(b.get(k) == v for k, v in kwargs.iteritems()):
461 builds.pop(b['id'])
462 result.append(b)
463 if result:
464 print colorize(title)
465 for b in sorted(result, key=sort_key):
466 print ' ', colorize('\t'.join(map(str, f(b))))
467
468 total = len(builds)
469 pop(status='COMPLETED', result='SUCCESS',
470 title='Successes:', color=Fore.GREEN,
471 f=lambda b: (get_name(b), b.get('url')))
472 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
473 title='Infra Failures:', color=Fore.MAGENTA,
474 f=lambda b: (get_name(b), b.get('url')))
475 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
476 title='Failures:', color=Fore.RED,
477 f=lambda b: (get_name(b), b.get('url')))
478 pop(status='COMPLETED', result='CANCELED',
479 title='Canceled:', color=Fore.MAGENTA,
480 f=lambda b: (get_name(b),))
481 pop(status='COMPLETED', result='FAILURE',
482 failure_reason='INVALID_BUILD_DEFINITION',
483 title='Wrong master/builder name:', color=Fore.MAGENTA,
484 f=lambda b: (get_name(b),))
485 pop(status='COMPLETED', result='FAILURE',
486 title='Other failures:',
487 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
488 pop(status='COMPLETED',
489 title='Other finished:',
490 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
491 pop(status='STARTED',
492 title='Started:', color=Fore.YELLOW,
493 f=lambda b: (get_name(b), b.get('url')))
494 pop(status='SCHEDULED',
495 title='Scheduled:',
496 f=lambda b: (get_name(b), 'id=%s' % b['id']))
497 # The last section is just in case buildbucket API changes OR there is a bug.
498 pop(title='Other:',
499 f=lambda b: (get_name(b), 'id=%s' % b['id']))
500 assert len(builds) == 0
501 print 'Total: %d tryjobs' % total
502
503
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000504def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
505 """Return the corresponding git ref if |base_url| together with |glob_spec|
506 matches the full |url|.
507
508 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
509 """
510 fetch_suburl, as_ref = glob_spec.split(':')
511 if allow_wildcards:
512 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
513 if glob_match:
514 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
515 # "branches/{472,597,648}/src:refs/remotes/svn/*".
516 branch_re = re.escape(base_url)
517 if glob_match.group(1):
518 branch_re += '/' + re.escape(glob_match.group(1))
519 wildcard = glob_match.group(2)
520 if wildcard == '*':
521 branch_re += '([^/]*)'
522 else:
523 # Escape and replace surrounding braces with parentheses and commas
524 # with pipe symbols.
525 wildcard = re.escape(wildcard)
526 wildcard = re.sub('^\\\\{', '(', wildcard)
527 wildcard = re.sub('\\\\,', '|', wildcard)
528 wildcard = re.sub('\\\\}$', ')', wildcard)
529 branch_re += wildcard
530 if glob_match.group(3):
531 branch_re += re.escape(glob_match.group(3))
532 match = re.match(branch_re, url)
533 if match:
534 return re.sub('\*$', match.group(1), as_ref)
535
536 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
537 if fetch_suburl:
538 full_url = base_url + '/' + fetch_suburl
539 else:
540 full_url = base_url
541 if full_url == url:
542 return as_ref
543 return None
544
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000545
iannucci@chromium.org79540052012-10-19 23:15:26 +0000546def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000547 """Prints statistics about the change to the user."""
548 # --no-ext-diff is broken in some versions of Git, so try to work around
549 # this by overriding the environment (but there is still a problem if the
550 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000551 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000552 if 'GIT_EXTERNAL_DIFF' in env:
553 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000554
555 if find_copies:
556 similarity_options = ['--find-copies-harder', '-l100000',
557 '-C%s' % similarity]
558 else:
559 similarity_options = ['-M%s' % similarity]
560
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000561 try:
562 stdout = sys.stdout.fileno()
563 except AttributeError:
564 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000565 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000566 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000567 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000568 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000569
570
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000571class BuildbucketResponseException(Exception):
572 pass
573
574
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000575class Settings(object):
576 def __init__(self):
577 self.default_server = None
578 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000579 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000580 self.is_git_svn = None
581 self.svn_branch = None
582 self.tree_status_url = None
583 self.viewvc_url = None
584 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000585 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000586 self.squash_gerrit_uploads = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000587 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000588 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000589 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000590 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000591
592 def LazyUpdateIfNeeded(self):
593 """Updates the settings from a codereview.settings file, if available."""
594 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000595 # The only value that actually changes the behavior is
596 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000597 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000598 error_ok=True
599 ).strip().lower()
600
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000601 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000602 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000603 LoadCodereviewSettingsFromFile(cr_settings_file)
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000604 # set updated to True to avoid infinite calling loop
tandrii@chromium.org18630d62016-03-04 12:06:02 +0000605 # through DownloadGerritHook
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000606 self.updated = True
tandrii@chromium.org18630d62016-03-04 12:06:02 +0000607 DownloadGerritHook(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000608 self.updated = True
609
610 def GetDefaultServerUrl(self, error_ok=False):
611 if not self.default_server:
612 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000613 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000614 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000615 if error_ok:
616 return self.default_server
617 if not self.default_server:
618 error_message = ('Could not find settings file. You must configure '
619 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000620 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000621 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000622 return self.default_server
623
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000624 @staticmethod
625 def GetRelativeRoot():
626 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000627
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000628 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000629 if self.root is None:
630 self.root = os.path.abspath(self.GetRelativeRoot())
631 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000632
633 def GetIsGitSvn(self):
634 """Return true if this repo looks like it's using git-svn."""
635 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000636 if self.GetPendingRefPrefix():
637 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
638 self.is_git_svn = False
639 else:
640 # If you have any "svn-remote.*" config keys, we think you're using svn.
641 self.is_git_svn = RunGitWithCode(
642 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000643 return self.is_git_svn
644
645 def GetSVNBranch(self):
646 if self.svn_branch is None:
647 if not self.GetIsGitSvn():
648 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
649
650 # Try to figure out which remote branch we're based on.
651 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000652 # 1) iterate through our branch history and find the svn URL.
653 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000654
655 # regexp matching the git-svn line that contains the URL.
656 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
657
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000658 # We don't want to go through all of history, so read a line from the
659 # pipe at a time.
660 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000661 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000662 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
663 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000664 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000665 for line in proc.stdout:
666 match = git_svn_re.match(line)
667 if match:
668 url = match.group(1)
669 proc.stdout.close() # Cut pipe.
670 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000671
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000672 if url:
673 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
674 remotes = RunGit(['config', '--get-regexp',
675 r'^svn-remote\..*\.url']).splitlines()
676 for remote in remotes:
677 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000678 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000679 remote = match.group(1)
680 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000681 rewrite_root = RunGit(
682 ['config', 'svn-remote.%s.rewriteRoot' % remote],
683 error_ok=True).strip()
684 if rewrite_root:
685 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000686 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000687 ['config', 'svn-remote.%s.fetch' % remote],
688 error_ok=True).strip()
689 if fetch_spec:
690 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
691 if self.svn_branch:
692 break
693 branch_spec = RunGit(
694 ['config', 'svn-remote.%s.branches' % remote],
695 error_ok=True).strip()
696 if branch_spec:
697 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
698 if self.svn_branch:
699 break
700 tag_spec = RunGit(
701 ['config', 'svn-remote.%s.tags' % remote],
702 error_ok=True).strip()
703 if tag_spec:
704 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
705 if self.svn_branch:
706 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000707
708 if not self.svn_branch:
709 DieWithError('Can\'t guess svn branch -- try specifying it on the '
710 'command line')
711
712 return self.svn_branch
713
714 def GetTreeStatusUrl(self, error_ok=False):
715 if not self.tree_status_url:
716 error_message = ('You must configure your tree status URL by running '
717 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000718 self.tree_status_url = self._GetRietveldConfig(
719 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000720 return self.tree_status_url
721
722 def GetViewVCUrl(self):
723 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000724 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000725 return self.viewvc_url
726
rmistry@google.com90752582014-01-14 21:04:50 +0000727 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000728 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000729
rmistry@google.com78948ed2015-07-08 23:09:57 +0000730 def GetIsSkipDependencyUpload(self, branch_name):
731 """Returns true if specified branch should skip dep uploads."""
732 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
733 error_ok=True)
734
rmistry@google.com5626a922015-02-26 14:03:30 +0000735 def GetRunPostUploadHook(self):
736 run_post_upload_hook = self._GetRietveldConfig(
737 'run-post-upload-hook', error_ok=True)
738 return run_post_upload_hook == "True"
739
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000740 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000741 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000742
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000743 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000744 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000745
ukai@chromium.orge8077812012-02-03 03:41:46 +0000746 def GetIsGerrit(self):
747 """Return true if this repo is assosiated with gerrit code review system."""
748 if self.is_gerrit is None:
749 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
750 return self.is_gerrit
751
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000752 def GetSquashGerritUploads(self):
753 """Return true if uploads to Gerrit should be squashed by default."""
754 if self.squash_gerrit_uploads is None:
755 self.squash_gerrit_uploads = (
756 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
757 error_ok=True).strip() == 'true')
758 return self.squash_gerrit_uploads
759
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000760 def GetGitEditor(self):
761 """Return the editor specified in the git config, or None if none is."""
762 if self.git_editor is None:
763 self.git_editor = self._GetConfig('core.editor', error_ok=True)
764 return self.git_editor or None
765
thestig@chromium.org44202a22014-03-11 19:22:18 +0000766 def GetLintRegex(self):
767 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
768 DEFAULT_LINT_REGEX)
769
770 def GetLintIgnoreRegex(self):
771 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
772 DEFAULT_LINT_IGNORE_REGEX)
773
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000774 def GetProject(self):
775 if not self.project:
776 self.project = self._GetRietveldConfig('project', error_ok=True)
777 return self.project
778
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000779 def GetForceHttpsCommitUrl(self):
780 if not self.force_https_commit_url:
781 self.force_https_commit_url = self._GetRietveldConfig(
782 'force-https-commit-url', error_ok=True)
783 return self.force_https_commit_url
784
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000785 def GetPendingRefPrefix(self):
786 if not self.pending_ref_prefix:
787 self.pending_ref_prefix = self._GetRietveldConfig(
788 'pending-ref-prefix', error_ok=True)
789 return self.pending_ref_prefix
790
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000791 def _GetRietveldConfig(self, param, **kwargs):
792 return self._GetConfig('rietveld.' + param, **kwargs)
793
rmistry@google.com78948ed2015-07-08 23:09:57 +0000794 def _GetBranchConfig(self, branch_name, param, **kwargs):
795 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
796
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000797 def _GetConfig(self, param, **kwargs):
798 self.LazyUpdateIfNeeded()
799 return RunGit(['config', param], **kwargs).strip()
800
801
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000802def ShortBranchName(branch):
803 """Convert a name like 'refs/heads/foo' to just 'foo'."""
804 return branch.replace('refs/heads/', '')
805
806
807class Changelist(object):
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000808 def __init__(self, branchref=None, issue=None, auth_config=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000809 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000810 global settings
811 if not settings:
812 # Happens when git_cl.py is used as a utility library.
813 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000814 settings.GetDefaultServerUrl()
815 self.branchref = branchref
816 if self.branchref:
817 self.branch = ShortBranchName(self.branchref)
818 else:
819 self.branch = None
820 self.rietveld_server = None
821 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000822 self.lookedup_issue = False
823 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000824 self.has_description = False
825 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000826 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000827 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000828 self.cc = None
829 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000830 self._auth_config = auth_config
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000831 self._props = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000832 self._remote = None
833 self._rpc_server = None
834
835 @property
836 def auth_config(self):
837 return self._auth_config
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000838
839 def GetCCList(self):
840 """Return the users cc'd on this CL.
841
842 Return is a string suitable for passing to gcl with the --cc flag.
843 """
844 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000845 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000846 more_cc = ','.join(self.watchers)
847 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
848 return self.cc
849
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000850 def GetCCListWithoutDefault(self):
851 """Return the users cc'd on this CL excluding default ones."""
852 if self.cc is None:
853 self.cc = ','.join(self.watchers)
854 return self.cc
855
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000856 def SetWatchers(self, watchers):
857 """Set the list of email addresses that should be cc'd based on the changed
858 files in this CL.
859 """
860 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000861
862 def GetBranch(self):
863 """Returns the short branch name, e.g. 'master'."""
864 if not self.branch:
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000865 branchref = RunGit(['symbolic-ref', 'HEAD'],
866 stderr=subprocess2.VOID, error_ok=True).strip()
867 if not branchref:
868 return None
869 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000870 self.branch = ShortBranchName(self.branchref)
871 return self.branch
872
873 def GetBranchRef(self):
874 """Returns the full branch name, e.g. 'refs/heads/master'."""
875 self.GetBranch() # Poke the lazy loader.
876 return self.branchref
877
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000878 @staticmethod
879 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000880 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000881 e.g. 'origin', 'refs/heads/master'
882 """
883 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000884 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
885 error_ok=True).strip()
886 if upstream_branch:
887 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
888 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000889 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
890 error_ok=True).strip()
891 if upstream_branch:
892 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000893 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000894 # Fall back on trying a git-svn upstream branch.
895 if settings.GetIsGitSvn():
896 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000897 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000898 # Else, try to guess the origin remote.
899 remote_branches = RunGit(['branch', '-r']).split()
900 if 'origin/master' in remote_branches:
901 # Fall back on origin/master if it exits.
902 remote = 'origin'
903 upstream_branch = 'refs/heads/master'
904 elif 'origin/trunk' in remote_branches:
905 # Fall back on origin/trunk if it exists. Generally a shared
906 # git-svn clone
907 remote = 'origin'
908 upstream_branch = 'refs/heads/trunk'
909 else:
910 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000911Either pass complete "git diff"-style arguments, like
912 git cl upload origin/master
913or verify this branch is set up to track another (via the --track argument to
914"git checkout -b ...").""")
915
916 return remote, upstream_branch
917
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000918 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000919 upstream_branch = self.GetUpstreamBranch()
920 if not BranchExists(upstream_branch):
921 DieWithError('The upstream for the current branch (%s) does not exist '
922 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +0000923 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000924 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000925
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000926 def GetUpstreamBranch(self):
927 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000928 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000929 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000930 upstream_branch = upstream_branch.replace('refs/heads/',
931 'refs/remotes/%s/' % remote)
932 upstream_branch = upstream_branch.replace('refs/branch-heads/',
933 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000934 self.upstream_branch = upstream_branch
935 return self.upstream_branch
936
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000937 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000938 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000939 remote, branch = None, self.GetBranch()
940 seen_branches = set()
941 while branch not in seen_branches:
942 seen_branches.add(branch)
943 remote, branch = self.FetchUpstreamTuple(branch)
944 branch = ShortBranchName(branch)
945 if remote != '.' or branch.startswith('refs/remotes'):
946 break
947 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000948 remotes = RunGit(['remote'], error_ok=True).split()
949 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000950 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000951 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000952 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000953 logging.warning('Could not determine which remote this change is '
954 'associated with, so defaulting to "%s". This may '
955 'not be what you want. You may prevent this message '
956 'by running "git svn info" as documented here: %s',
957 self._remote,
958 GIT_INSTRUCTIONS_URL)
959 else:
960 logging.warn('Could not determine which remote this change is '
961 'associated with. You may prevent this message by '
962 'running "git svn info" as documented here: %s',
963 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000964 branch = 'HEAD'
965 if branch.startswith('refs/remotes'):
966 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000967 elif branch.startswith('refs/branch-heads/'):
968 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000969 else:
970 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000971 return self._remote
972
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000973 def GitSanityChecks(self, upstream_git_obj):
974 """Checks git repo status and ensures diff is from local commits."""
975
sbc@chromium.org79706062015-01-14 21:18:12 +0000976 if upstream_git_obj is None:
977 if self.GetBranch() is None:
978 print >> sys.stderr, (
dpranke@chromium.orgee87f582015-07-31 18:46:25 +0000979 'ERROR: unable to determine current branch (detached HEAD?)')
sbc@chromium.org79706062015-01-14 21:18:12 +0000980 else:
981 print >> sys.stderr, (
982 'ERROR: no upstream branch')
983 return False
984
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000985 # Verify the commit we're diffing against is in our current branch.
986 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
987 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
988 if upstream_sha != common_ancestor:
989 print >> sys.stderr, (
990 'ERROR: %s is not in the current branch. You may need to rebase '
991 'your tracking branch' % upstream_sha)
992 return False
993
994 # List the commits inside the diff, and verify they are all local.
995 commits_in_diff = RunGit(
996 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
997 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
998 remote_branch = remote_branch.strip()
999 if code != 0:
1000 _, remote_branch = self.GetRemoteBranch()
1001
1002 commits_in_remote = RunGit(
1003 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1004
1005 common_commits = set(commits_in_diff) & set(commits_in_remote)
1006 if common_commits:
1007 print >> sys.stderr, (
1008 'ERROR: Your diff contains %d commits already in %s.\n'
1009 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1010 'the diff. If you are using a custom git flow, you can override'
1011 ' the reference used for this check with "git config '
1012 'gitcl.remotebranch <git-ref>".' % (
1013 len(common_commits), remote_branch, upstream_git_obj))
1014 return False
1015 return True
1016
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001017 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001018 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001019
1020 Returns None if it is not set.
1021 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001022 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
1023 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001024
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001025 def GetGitSvnRemoteUrl(self):
1026 """Return the configured git-svn remote URL parsed from git svn info.
1027
1028 Returns None if it is not set.
1029 """
1030 # URL is dependent on the current directory.
1031 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1032 if data:
1033 keys = dict(line.split(': ', 1) for line in data.splitlines()
1034 if ': ' in line)
1035 return keys.get('URL', None)
1036 return None
1037
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001038 def GetRemoteUrl(self):
1039 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1040
1041 Returns None if there is no remote.
1042 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001043 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001044 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1045
1046 # If URL is pointing to a local directory, it is probably a git cache.
1047 if os.path.isdir(url):
1048 url = RunGit(['config', 'remote.%s.url' % remote],
1049 error_ok=True,
1050 cwd=url).strip()
1051 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001052
1053 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001054 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001055 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001056 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001057 self.issue = int(issue) or None if issue else None
1058 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001059 return self.issue
1060
1061 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +00001062 if not self.rietveld_server:
1063 # If we're on a branch then get the server potentially associated
1064 # with that branch.
1065 if self.GetIssue():
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001066 rietveld_server_config = self._RietveldServer()
1067 if rietveld_server_config:
1068 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1069 ['config', rietveld_server_config], error_ok=True).strip())
evan@chromium.org0af9b702012-02-11 00:42:16 +00001070 if not self.rietveld_server:
1071 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001072 return self.rietveld_server
1073
1074 def GetIssueURL(self):
1075 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001076 if not self.GetIssue():
1077 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001078 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
1079
1080 def GetDescription(self, pretty=False):
1081 if not self.has_description:
1082 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +00001083 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +00001084 try:
1085 self.description = self.RpcServer().get_description(issue).strip()
maruel@chromium.org85616e02014-07-28 15:37:55 +00001086 except urllib2.HTTPError as e:
miket@chromium.org183df1a2012-01-04 19:44:55 +00001087 if e.code == 404:
1088 DieWithError(
1089 ('\nWhile fetching the description for issue %d, received a '
1090 '404 (not found)\n'
1091 'error. It is likely that you deleted this '
1092 'issue on the server. If this is the\n'
1093 'case, please run\n\n'
1094 ' git cl issue 0\n\n'
1095 'to clear the association with the deleted issue. Then run '
1096 'this command again.') % issue)
1097 else:
1098 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +00001099 '\nFailed to fetch issue description. HTTP error %d' % e.code)
maruel@chromium.org85616e02014-07-28 15:37:55 +00001100 except urllib2.URLError as e:
1101 print >> sys.stderr, (
1102 'Warning: Failed to retrieve CL description due to network '
1103 'failure.')
1104 self.description = ''
1105
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001106 self.has_description = True
1107 if pretty:
1108 wrapper = textwrap.TextWrapper()
1109 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1110 return wrapper.fill(self.description)
1111 return self.description
1112
1113 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001114 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001115 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001116 patchset = RunGit(['config', self._PatchsetSetting()],
1117 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001118 self.patchset = int(patchset) or None if patchset else None
1119 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001120 return self.patchset
1121
1122 def SetPatchset(self, patchset):
1123 """Set this branch's patchset. If patchset=0, clears the patchset."""
1124 if patchset:
1125 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001126 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001127 else:
1128 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001129 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001130 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001131
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001132 def GetMostRecentPatchset(self):
1133 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +00001134
1135 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001136 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001137 '/download/issue%s_%s.diff' % (issue, patchset))
1138
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001139 def GetIssueProperties(self):
1140 if self._props is None:
1141 issue = self.GetIssue()
1142 if not issue:
1143 self._props = {}
1144 else:
1145 self._props = self.RpcServer().get_issue_properties(issue, True)
1146 return self._props
1147
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001148 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001149 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001150
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001151 def AddComment(self, message):
1152 return self.RpcServer().add_comment(self.GetIssue(), message)
1153
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001154 def SetIssue(self, issue):
1155 """Set this branch's issue. If issue=0, clears the issue."""
1156 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001157 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001158 RunGit(['config', self._IssueSetting(), str(issue)])
1159 if self.rietveld_server:
1160 RunGit(['config', self._RietveldServer(), self.rietveld_server])
1161 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +00001162 current_issue = self.GetIssue()
1163 if current_issue:
1164 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001165 self.issue = None
1166 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001167
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001168 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001169 if not self.GitSanityChecks(upstream_branch):
1170 DieWithError('\nGit sanity check failure')
1171
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001172 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001173 if not root:
1174 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001175 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001176
1177 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001178 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001179 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001180 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001181 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001182 except subprocess2.CalledProcessError:
1183 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001184 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001185 'This branch probably doesn\'t exist anymore. To reset the\n'
1186 'tracking branch, please run\n'
1187 ' git branch --set-upstream %s trunk\n'
1188 'replacing trunk with origin/master or the relevant branch') %
1189 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001190
maruel@chromium.org52424302012-08-29 15:14:30 +00001191 issue = self.GetIssue()
1192 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001193 if issue:
1194 description = self.GetDescription()
1195 else:
1196 # If the change was never uploaded, use the log messages of all commits
1197 # up to the branch point, as git cl upload will prefill the description
1198 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001199 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1200 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001201
1202 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001203 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001204 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001205 name,
1206 description,
1207 absroot,
1208 files,
1209 issue,
1210 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001211 author,
1212 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001213
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001214 def GetStatus(self):
1215 """Apply a rough heuristic to give a simple summary of an issue's review
1216 or CQ status, assuming adherence to a common workflow.
1217
1218 Returns None if no issue for this branch, or one of the following keywords:
1219 * 'error' - error from review tool (including deleted issues)
1220 * 'unsent' - not sent for review
1221 * 'waiting' - waiting for review
1222 * 'reply' - waiting for owner to reply to review
1223 * 'lgtm' - LGTM from at least one approved reviewer
1224 * 'commit' - in the commit queue
1225 * 'closed' - closed
1226 """
1227 if not self.GetIssue():
1228 return None
1229
1230 try:
1231 props = self.GetIssueProperties()
1232 except urllib2.HTTPError:
1233 return 'error'
1234
1235 if props.get('closed'):
1236 # Issue is closed.
1237 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001238 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001239 # Issue is in the commit queue.
1240 return 'commit'
1241
1242 try:
1243 reviewers = self.GetApprovingReviewers()
1244 except urllib2.HTTPError:
1245 return 'error'
1246
1247 if reviewers:
1248 # Was LGTM'ed.
1249 return 'lgtm'
1250
1251 messages = props.get('messages') or []
1252
1253 if not messages:
1254 # No message was sent.
1255 return 'unsent'
1256 if messages[-1]['sender'] != props.get('owner_email'):
1257 # Non-LGTM reply from non-owner
1258 return 'reply'
1259 return 'waiting'
1260
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001261 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001262 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001263
1264 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001265 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001266 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001267 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001268 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001269 except presubmit_support.PresubmitFailure, e:
1270 DieWithError(
1271 ('%s\nMaybe your depot_tools is out of date?\n'
1272 'If all fails, contact maruel@') % e)
1273
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001274 def UpdateDescription(self, description):
1275 self.description = description
1276 return self.RpcServer().update_description(
1277 self.GetIssue(), self.description)
1278
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001279 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +00001280 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001281 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001282
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001283 def SetFlag(self, flag, value):
1284 """Patchset must match."""
1285 if not self.GetPatchset():
1286 DieWithError('The patchset needs to match. Send another patchset.')
1287 try:
1288 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001289 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001290 except urllib2.HTTPError, e:
1291 if e.code == 404:
1292 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1293 if e.code == 403:
1294 DieWithError(
1295 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1296 'match?') % (self.GetIssue(), self.GetPatchset()))
1297 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001298
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001299 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001300 """Returns an upload.RpcServer() to access this review's rietveld instance.
1301 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001302 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001303 self._rpc_server = rietveld.CachingRietveld(
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001304 self.GetRietveldServer(),
1305 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001306 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001307
1308 def _IssueSetting(self):
1309 """Return the git setting that stores this change's issue."""
1310 return 'branch.%s.rietveldissue' % self.GetBranch()
1311
1312 def _PatchsetSetting(self):
1313 """Return the git setting that stores this change's most recent patchset."""
1314 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1315
1316 def _RietveldServer(self):
1317 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001318 branch = self.GetBranch()
1319 if branch:
1320 return 'branch.%s.rietveldserver' % branch
1321 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001322
1323
1324def GetCodereviewSettingsInteractively():
1325 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001326 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001327 server = settings.GetDefaultServerUrl(error_ok=True)
1328 prompt = 'Rietveld server (host[:port])'
1329 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +00001330 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001331 if not server and not newserver:
1332 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001333 if newserver:
1334 newserver = gclient_utils.UpgradeToHttps(newserver)
1335 if newserver != server:
1336 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001337
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001338 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001339 prompt = caption
1340 if initial:
1341 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +00001342 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001343 if new_val == 'x':
1344 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001345 elif new_val:
1346 if is_url:
1347 new_val = gclient_utils.UpgradeToHttps(new_val)
1348 if new_val != initial:
1349 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001350
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001351 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001352 SetProperty(settings.GetDefaultPrivateFlag(),
1353 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001354 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001355 'tree-status-url', False)
1356 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +00001357 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
rmistry@google.com5626a922015-02-26 14:03:30 +00001358 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
1359 'run-post-upload-hook', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001360
1361 # TODO: configure a default branch to diff against, rather than this
1362 # svn-based hackery.
1363
1364
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001365class ChangeDescription(object):
1366 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001367 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00001368 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001369
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001370 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00001371 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001372
agable@chromium.org42c20792013-09-12 17:34:49 +00001373 @property # www.logilab.org/ticket/89786
1374 def description(self): # pylint: disable=E0202
1375 return '\n'.join(self._description_lines)
1376
1377 def set_description(self, desc):
1378 if isinstance(desc, basestring):
1379 lines = desc.splitlines()
1380 else:
1381 lines = [line.rstrip() for line in desc]
1382 while lines and not lines[0]:
1383 lines.pop(0)
1384 while lines and not lines[-1]:
1385 lines.pop(-1)
1386 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001387
piman@chromium.org336f9122014-09-04 02:16:55 +00001388 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001389 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001390 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001391 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001392 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001393 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001394
agable@chromium.org42c20792013-09-12 17:34:49 +00001395 # Get the set of R= and TBR= lines and remove them from the desciption.
1396 regexp = re.compile(self.R_LINE)
1397 matches = [regexp.match(line) for line in self._description_lines]
1398 new_desc = [l for i, l in enumerate(self._description_lines)
1399 if not matches[i]]
1400 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001401
agable@chromium.org42c20792013-09-12 17:34:49 +00001402 # Construct new unified R= and TBR= lines.
1403 r_names = []
1404 tbr_names = []
1405 for match in matches:
1406 if not match:
1407 continue
1408 people = cleanup_list([match.group(2).strip()])
1409 if match.group(1) == 'TBR':
1410 tbr_names.extend(people)
1411 else:
1412 r_names.extend(people)
1413 for name in r_names:
1414 if name not in reviewers:
1415 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001416 if add_owners_tbr:
1417 owners_db = owners.Database(change.RepositoryRoot(),
1418 fopen=file, os_path=os.path, glob=glob.glob)
1419 all_reviewers = set(tbr_names + reviewers)
1420 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1421 all_reviewers)
1422 tbr_names.extend(owners_db.reviewers_for(missing_files,
1423 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001424 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1425 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1426
1427 # Put the new lines in the description where the old first R= line was.
1428 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1429 if 0 <= line_loc < len(self._description_lines):
1430 if new_tbr_line:
1431 self._description_lines.insert(line_loc, new_tbr_line)
1432 if new_r_line:
1433 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001434 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001435 if new_r_line:
1436 self.append_footer(new_r_line)
1437 if new_tbr_line:
1438 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001439
1440 def prompt(self):
1441 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001442 self.set_description([
1443 '# Enter a description of the change.',
1444 '# This will be displayed on the codereview site.',
1445 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00001446 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00001447 '--------------------',
1448 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001449
agable@chromium.org42c20792013-09-12 17:34:49 +00001450 regexp = re.compile(self.BUG_LINE)
1451 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00001452 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00001453 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001454 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001455 if not content:
1456 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001457 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001458
1459 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001460 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1461 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001462 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001463 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001464
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001465 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001466 if self._description_lines:
1467 # Add an empty line if either the last line or the new line isn't a tag.
1468 last_line = self._description_lines[-1]
1469 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1470 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1471 self._description_lines.append('')
1472 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001473
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001474 def get_reviewers(self):
1475 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001476 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1477 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001478 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001479
1480
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001481def get_approving_reviewers(props):
1482 """Retrieves the reviewers that approved a CL from the issue properties with
1483 messages.
1484
1485 Note that the list may contain reviewers that are not committer, thus are not
1486 considered by the CQ.
1487 """
1488 return sorted(
1489 set(
1490 message['sender']
1491 for message in props['messages']
1492 if message['approval'] and message['sender'] in props['reviewers']
1493 )
1494 )
1495
1496
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001497def FindCodereviewSettingsFile(filename='codereview.settings'):
1498 """Finds the given file starting in the cwd and going up.
1499
1500 Only looks up to the top of the repository unless an
1501 'inherit-review-settings-ok' file exists in the root of the repository.
1502 """
1503 inherit_ok_file = 'inherit-review-settings-ok'
1504 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001505 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001506 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1507 root = '/'
1508 while True:
1509 if filename in os.listdir(cwd):
1510 if os.path.isfile(os.path.join(cwd, filename)):
1511 return open(os.path.join(cwd, filename))
1512 if cwd == root:
1513 break
1514 cwd = os.path.dirname(cwd)
1515
1516
1517def LoadCodereviewSettingsFromFile(fileobj):
1518 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001519 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001520
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001521 def SetProperty(name, setting, unset_error_ok=False):
1522 fullname = 'rietveld.' + name
1523 if setting in keyvals:
1524 RunGit(['config', fullname, keyvals[setting]])
1525 else:
1526 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1527
1528 SetProperty('server', 'CODE_REVIEW_SERVER')
1529 # Only server setting is required. Other settings can be absent.
1530 # In that case, we ignore errors raised during option deletion attempt.
1531 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001532 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001533 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1534 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001535 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001536 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001537 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
1538 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001539 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001540 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001541 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00001542 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
1543 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001544
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001545 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001546 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001547
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00001548 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
1549 RunGit(['config', 'gerrit.squash-uploads',
1550 keyvals['GERRIT_SQUASH_UPLOADS']])
1551
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001552 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1553 #should be of the form
1554 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1555 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1556 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1557 keyvals['ORIGIN_URL_CONFIG']])
1558
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001559
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001560def urlretrieve(source, destination):
1561 """urllib is broken for SSL connections via a proxy therefore we
1562 can't use urllib.urlretrieve()."""
1563 with open(destination, 'w') as f:
1564 f.write(urllib2.urlopen(source).read())
1565
1566
ukai@chromium.org712d6102013-11-27 00:52:58 +00001567def hasSheBang(fname):
1568 """Checks fname is a #! script."""
1569 with open(fname) as f:
1570 return f.read(2).startswith('#!')
1571
1572
tandrii@chromium.org18630d62016-03-04 12:06:02 +00001573def DownloadGerritHook(force):
1574 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001575
1576 Args:
1577 force: True to update hooks. False to install hooks if not present.
1578 """
1579 if not settings.GetIsGerrit():
1580 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001581 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001582 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1583 if not os.access(dst, os.X_OK):
1584 if os.path.exists(dst):
1585 if not force:
1586 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001587 try:
tandrii@chromium.org18630d62016-03-04 12:06:02 +00001588 print(
1589 'WARNING: installing Gerrit commit-msg hook.\n'
1590 ' This behavior of git cl will soon be disabled.\n'
1591 ' See bug http://crbug.com/579176.')
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001592 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001593 if not hasSheBang(dst):
1594 DieWithError('Not a script: %s\n'
1595 'You need to download from\n%s\n'
1596 'into .git/hooks/commit-msg and '
1597 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001598 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1599 except Exception:
1600 if os.path.exists(dst):
1601 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001602 DieWithError('\nFailed to download hooks.\n'
1603 'You need to download from\n%s\n'
1604 'into .git/hooks/commit-msg and '
1605 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001606
tandrii@chromium.orgc55295c2016-03-04 15:54:59 +00001607# TODO(tandrii): remove this once repos which call this method directly are
1608# upgraded.
1609DownloadHooks = DownloadGerritHook
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001610
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001611@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001612def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001613 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001614
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001615 parser.add_option('--activate-update', action='store_true',
1616 help='activate auto-updating [rietveld] section in '
1617 '.git/config')
1618 parser.add_option('--deactivate-update', action='store_true',
1619 help='deactivate auto-updating [rietveld] section in '
1620 '.git/config')
1621 options, args = parser.parse_args(args)
1622
1623 if options.deactivate_update:
1624 RunGit(['config', 'rietveld.autoupdate', 'false'])
1625 return
1626
1627 if options.activate_update:
1628 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1629 return
1630
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001631 if len(args) == 0:
1632 GetCodereviewSettingsInteractively()
tandrii@chromium.org18630d62016-03-04 12:06:02 +00001633 DownloadGerritHook(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001634 return 0
1635
1636 url = args[0]
1637 if not url.endswith('codereview.settings'):
1638 url = os.path.join(url, 'codereview.settings')
1639
1640 # Load code review settings and download hooks (if available).
1641 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
tandrii@chromium.org18630d62016-03-04 12:06:02 +00001642 DownloadGerritHook(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001643 return 0
1644
1645
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001646def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001647 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001648 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1649 branch = ShortBranchName(branchref)
1650 _, args = parser.parse_args(args)
1651 if not args:
1652 print("Current base-url:")
1653 return RunGit(['config', 'branch.%s.base-url' % branch],
1654 error_ok=False).strip()
1655 else:
1656 print("Setting base-url to %s" % args[0])
1657 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1658 error_ok=False).strip()
1659
1660
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001661def color_for_status(status):
1662 """Maps a Changelist status to color, for CMDstatus and other tools."""
1663 return {
1664 'unsent': Fore.RED,
1665 'waiting': Fore.BLUE,
1666 'reply': Fore.YELLOW,
1667 'lgtm': Fore.GREEN,
1668 'commit': Fore.MAGENTA,
1669 'closed': Fore.CYAN,
1670 'error': Fore.WHITE,
1671 }.get(status, Fore.WHITE)
1672
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001673def fetch_cl_status(branch, auth_config=None):
1674 """Fetches information for an issue and returns (branch, issue, status)."""
1675 cl = Changelist(branchref=branch, auth_config=auth_config)
1676 url = cl.GetIssueURL()
1677 status = cl.GetStatus()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001678
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001679 if url and (not status or status == 'error'):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001680 # The issue probably doesn't exist anymore.
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001681 url += ' (broken)'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001682
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001683 return (branch, url, status)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001684
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001685def get_cl_statuses(
1686 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001687 """Returns a blocking iterable of (branch, issue, color) for given branches.
1688
1689 If fine_grained is true, this will fetch CL statuses from the server.
1690 Otherwise, simply indicate if there's a matching url for the given branches.
1691
1692 If max_processes is specified, it is used as the maximum number of processes
1693 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
1694 spawned.
1695 """
1696 # Silence upload.py otherwise it becomes unwieldly.
1697 upload.verbosity = 0
1698
1699 if fine_grained:
1700 # Process one branch synchronously to work through authentication, then
1701 # spawn processes to process all the other branches in parallel.
1702 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001703 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
1704 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001705
1706 branches_to_fetch = branches[1:]
1707 pool = ThreadPool(
1708 min(max_processes, len(branches_to_fetch))
1709 if max_processes is not None
1710 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001711 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001712 yield x
1713 else:
1714 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1715 for b in branches:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001716 cl = Changelist(branchref=b, auth_config=auth_config)
1717 url = cl.GetIssueURL()
1718 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001719
rmistry@google.com2dd99862015-06-22 12:22:18 +00001720
1721def upload_branch_deps(cl, args):
1722 """Uploads CLs of local branches that are dependents of the current branch.
1723
1724 If the local branch dependency tree looks like:
1725 test1 -> test2.1 -> test3.1
1726 -> test3.2
1727 -> test2.2 -> test3.3
1728
1729 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
1730 run on the dependent branches in this order:
1731 test2.1, test3.1, test3.2, test2.2, test3.3
1732
1733 Note: This function does not rebase your local dependent branches. Use it when
1734 you make a change to the parent branch that will not conflict with its
1735 dependent branches, and you would like their dependencies updated in
1736 Rietveld.
1737 """
1738 if git_common.is_dirty_git_tree('upload-branch-deps'):
1739 return 1
1740
1741 root_branch = cl.GetBranch()
1742 if root_branch is None:
1743 DieWithError('Can\'t find dependent branches from detached HEAD state. '
1744 'Get on a branch!')
1745 if not cl.GetIssue() or not cl.GetPatchset():
1746 DieWithError('Current branch does not have an uploaded CL. We cannot set '
1747 'patchset dependencies without an uploaded CL.')
1748
1749 branches = RunGit(['for-each-ref',
1750 '--format=%(refname:short) %(upstream:short)',
1751 'refs/heads'])
1752 if not branches:
1753 print('No local branches found.')
1754 return 0
1755
1756 # Create a dictionary of all local branches to the branches that are dependent
1757 # on it.
1758 tracked_to_dependents = collections.defaultdict(list)
1759 for b in branches.splitlines():
1760 tokens = b.split()
1761 if len(tokens) == 2:
1762 branch_name, tracked = tokens
1763 tracked_to_dependents[tracked].append(branch_name)
1764
1765 print
1766 print 'The dependent local branches of %s are:' % root_branch
1767 dependents = []
1768 def traverse_dependents_preorder(branch, padding=''):
1769 dependents_to_process = tracked_to_dependents.get(branch, [])
1770 padding += ' '
1771 for dependent in dependents_to_process:
1772 print '%s%s' % (padding, dependent)
1773 dependents.append(dependent)
1774 traverse_dependents_preorder(dependent, padding)
1775 traverse_dependents_preorder(root_branch)
1776 print
1777
1778 if not dependents:
1779 print 'There are no dependent local branches for %s' % root_branch
1780 return 0
1781
1782 print ('This command will checkout all dependent branches and run '
1783 '"git cl upload".')
1784 ask_for_data('[Press enter to continue or ctrl-C to quit]')
1785
andybons@chromium.org962f9462016-02-03 20:00:42 +00001786 # Add a default patchset title to all upload calls in Rietveld.
1787 if not settings.GetIsGerrit():
1788 args.extend(['-t', 'Updated patchset dependency'])
1789
rmistry@google.com2dd99862015-06-22 12:22:18 +00001790 # Record all dependents that failed to upload.
1791 failures = {}
1792 # Go through all dependents, checkout the branch and upload.
1793 try:
1794 for dependent_branch in dependents:
1795 print
1796 print '--------------------------------------'
1797 print 'Running "git cl upload" from %s:' % dependent_branch
1798 RunGit(['checkout', '-q', dependent_branch])
1799 print
1800 try:
1801 if CMDupload(OptionParser(), args) != 0:
1802 print 'Upload failed for %s!' % dependent_branch
1803 failures[dependent_branch] = 1
1804 except: # pylint: disable=W0702
1805 failures[dependent_branch] = 1
1806 print
1807 finally:
1808 # Swap back to the original root branch.
1809 RunGit(['checkout', '-q', root_branch])
1810
1811 print
1812 print 'Upload complete for dependent branches!'
1813 for dependent_branch in dependents:
1814 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
1815 print ' %s : %s' % (dependent_branch, upload_status)
1816 print
1817
1818 return 0
1819
1820
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001821def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001822 """Show status of changelists.
1823
1824 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001825 - Red not sent for review or broken
1826 - Blue waiting for review
1827 - Yellow waiting for you to reply to review
1828 - Green LGTM'ed
1829 - Magenta in the commit queue
1830 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001831
1832 Also see 'git cl comments'.
1833 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001834 parser.add_option('--field',
1835 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001836 parser.add_option('-f', '--fast', action='store_true',
1837 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001838 parser.add_option(
1839 '-j', '--maxjobs', action='store', type=int,
1840 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001841
1842 auth.add_auth_options(parser)
1843 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001844 if args:
1845 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001846 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001847
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001848 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001849 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001850 if options.field.startswith('desc'):
1851 print cl.GetDescription()
1852 elif options.field == 'id':
1853 issueid = cl.GetIssue()
1854 if issueid:
1855 print issueid
1856 elif options.field == 'patch':
1857 patchset = cl.GetPatchset()
1858 if patchset:
1859 print patchset
1860 elif options.field == 'url':
1861 url = cl.GetIssueURL()
1862 if url:
1863 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001864 return 0
1865
1866 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1867 if not branches:
1868 print('No local branch found.')
1869 return 0
1870
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001871 changes = (
1872 Changelist(branchref=b, auth_config=auth_config)
1873 for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001874 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001875 alignment = max(5, max(len(b) for b in branches))
1876 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001877 output = get_cl_statuses(branches,
1878 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001879 max_processes=options.maxjobs,
1880 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001881
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001882 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001883 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001884 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001885 while branch not in branch_statuses:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001886 b, i, status = output.next()
1887 branch_statuses[b] = (i, status)
1888 issue_url, status = branch_statuses.pop(branch)
1889 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001890 reset = Fore.RESET
1891 if not sys.stdout.isatty():
1892 color = ''
1893 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001894 status_str = '(%s)' % status if status else ''
1895 print ' %*s : %s%s %s%s' % (
1896 alignment, ShortBranchName(branch), color, issue_url, status_str,
1897 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001898
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001899 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001900 print
1901 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001902 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00001903 if not cl.GetIssue():
1904 print 'No issue assigned.'
1905 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001906 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00001907 if not options.fast:
1908 print 'Issue description:'
1909 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001910 return 0
1911
1912
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001913def colorize_CMDstatus_doc():
1914 """To be called once in main() to add colors to git cl status help."""
1915 colors = [i for i in dir(Fore) if i[0].isupper()]
1916
1917 def colorize_line(line):
1918 for color in colors:
1919 if color in line.upper():
1920 # Extract whitespaces first and the leading '-'.
1921 indent = len(line) - len(line.lstrip(' ')) + 1
1922 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1923 return line
1924
1925 lines = CMDstatus.__doc__.splitlines()
1926 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1927
1928
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001929@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001930def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001931 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001932
1933 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001934 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00001935 parser.add_option('-r', '--reverse', action='store_true',
1936 help='Lookup the branch(es) for the specified issues. If '
1937 'no issues are specified, all branches with mapped '
1938 'issues will be listed.')
1939 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001940
dnj@chromium.org406c4402015-03-03 17:22:28 +00001941 if options.reverse:
1942 branches = RunGit(['for-each-ref', 'refs/heads',
1943 '--format=%(refname:short)']).splitlines()
1944
1945 # Reverse issue lookup.
1946 issue_branch_map = {}
1947 for branch in branches:
1948 cl = Changelist(branchref=branch)
1949 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
1950 if not args:
1951 args = sorted(issue_branch_map.iterkeys())
1952 for issue in args:
1953 if not issue:
1954 continue
1955 print 'Branch for issue number %s: %s' % (
1956 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
1957 else:
1958 cl = Changelist()
1959 if len(args) > 0:
1960 try:
1961 issue = int(args[0])
1962 except ValueError:
1963 DieWithError('Pass a number to set the issue or none to list it.\n'
1964 'Maybe you want to run git cl status?')
1965 cl.SetIssue(issue)
1966 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001967 return 0
1968
1969
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001970def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001971 """Shows or posts review comments for any changelist."""
1972 parser.add_option('-a', '--add-comment', dest='comment',
1973 help='comment to add to an issue')
1974 parser.add_option('-i', dest='issue',
1975 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00001976 parser.add_option('-j', '--json-file',
1977 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001978 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001979 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001980 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001981
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001982 issue = None
1983 if options.issue:
1984 try:
1985 issue = int(options.issue)
1986 except ValueError:
1987 DieWithError('A review issue id is expected to be a number')
1988
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001989 cl = Changelist(issue=issue, auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001990
1991 if options.comment:
1992 cl.AddComment(options.comment)
1993 return 0
1994
1995 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00001996 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00001997 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00001998 summary.append({
1999 'date': message['date'],
2000 'lgtm': False,
2001 'message': message['text'],
2002 'not_lgtm': False,
2003 'sender': message['sender'],
2004 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002005 if message['disapproval']:
2006 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00002007 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002008 elif message['approval']:
2009 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00002010 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002011 elif message['sender'] == data['owner_email']:
2012 color = Fore.MAGENTA
2013 else:
2014 color = Fore.BLUE
2015 print '\n%s%s %s%s' % (
2016 color, message['date'].split('.', 1)[0], message['sender'],
2017 Fore.RESET)
2018 if message['text'].strip():
2019 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00002020 if options.json_file:
2021 with open(options.json_file, 'wb') as f:
2022 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002023 return 0
2024
2025
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002026def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002027 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00002028 parser.add_option('-d', '--display', action='store_true',
2029 help='Display the description instead of opening an editor')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002030 auth.add_auth_options(parser)
2031 options, _ = parser.parse_args(args)
2032 auth_config = auth.extract_auth_config_from_options(options)
2033 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002034 if not cl.GetIssue():
2035 DieWithError('This branch has no associated changelist.')
2036 description = ChangeDescription(cl.GetDescription())
smut@google.com34fb6b12015-07-13 20:03:26 +00002037 if options.display:
2038 print description.description
2039 return 0
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002040 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00002041 if cl.GetDescription() != description.description:
2042 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002043 return 0
2044
2045
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002046def CreateDescriptionFromLog(args):
2047 """Pulls out the commit log to use as a base for the CL description."""
2048 log_args = []
2049 if len(args) == 1 and not args[0].endswith('.'):
2050 log_args = [args[0] + '..']
2051 elif len(args) == 1 and args[0].endswith('...'):
2052 log_args = [args[0][:-1]]
2053 elif len(args) == 2:
2054 log_args = [args[0] + '..' + args[1]]
2055 else:
2056 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00002057 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002058
2059
thestig@chromium.org44202a22014-03-11 19:22:18 +00002060def CMDlint(parser, args):
2061 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002062 parser.add_option('--filter', action='append', metavar='-x,+y',
2063 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002064 auth.add_auth_options(parser)
2065 options, args = parser.parse_args(args)
2066 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002067
2068 # Access to a protected member _XX of a client class
2069 # pylint: disable=W0212
2070 try:
2071 import cpplint
2072 import cpplint_chromium
2073 except ImportError:
2074 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
2075 return 1
2076
2077 # Change the current working directory before calling lint so that it
2078 # shows the correct base.
2079 previous_cwd = os.getcwd()
2080 os.chdir(settings.GetRoot())
2081 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002082 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002083 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
2084 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00002085 if not files:
2086 print "Cannot lint an empty CL"
2087 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00002088
2089 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002090 command = args + files
2091 if options.filter:
2092 command = ['--filter=' + ','.join(options.filter)] + command
2093 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002094
2095 white_regex = re.compile(settings.GetLintRegex())
2096 black_regex = re.compile(settings.GetLintIgnoreRegex())
2097 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
2098 for filename in filenames:
2099 if white_regex.match(filename):
2100 if black_regex.match(filename):
2101 print "Ignoring file %s" % filename
2102 else:
2103 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
2104 extra_check_functions)
2105 else:
2106 print "Skipping file %s" % filename
2107 finally:
2108 os.chdir(previous_cwd)
2109 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
2110 if cpplint._cpplint_state.error_count != 0:
2111 return 1
2112 return 0
2113
2114
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002115def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002116 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002117 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002118 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002119 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00002120 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002121 auth.add_auth_options(parser)
2122 options, args = parser.parse_args(args)
2123 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002124
sbc@chromium.org71437c02015-04-09 19:29:40 +00002125 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00002126 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002127 return 1
2128
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002129 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002130 if args:
2131 base_branch = args[0]
2132 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002133 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002134 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002135
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002136 cl.RunHook(
2137 committing=not options.upload,
2138 may_prompt=False,
2139 verbose=options.verbose,
2140 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00002141 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002142
2143
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002144def AddChangeIdToCommitMessage(options, args):
2145 """Re-commits using the current message, assumes the commit hook is in
2146 place.
2147 """
2148 log_desc = options.message or CreateDescriptionFromLog(args)
2149 git_command = ['commit', '--amend', '-m', log_desc]
2150 RunGit(git_command)
2151 new_log_desc = CreateDescriptionFromLog(args)
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00002152 if git_footers.get_footer_change_id(new_log_desc):
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002153 print 'git-cl: Added Change-Id to commit message.'
2154 else:
2155 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
2156
2157
tandrii@chromium.org65874e12016-03-04 12:03:02 +00002158def GenerateGerritChangeId(message):
2159 """Returns Ixxxxxx...xxx change id.
2160
2161 Works the same way as
2162 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
2163 but can be called on demand on all platforms.
2164
2165 The basic idea is to generate git hash of a state of the tree, original commit
2166 message, author/committer info and timestamps.
2167 """
2168 lines = []
2169 tree_hash = RunGitSilent(['write-tree'])
2170 lines.append('tree %s' % tree_hash.strip())
2171 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
2172 if code == 0:
2173 lines.append('parent %s' % parent.strip())
2174 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
2175 lines.append('author %s' % author.strip())
2176 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
2177 lines.append('committer %s' % committer.strip())
2178 lines.append('')
2179 # Note: Gerrit's commit-hook actually cleans message of some lines and
2180 # whitespace. This code is not doing this, but it clearly won't decrease
2181 # entropy.
2182 lines.append(message)
2183 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
2184 stdin='\n'.join(lines))
2185 return 'I%s' % change_hash.strip()
2186
2187
piman@chromium.org336f9122014-09-04 02:16:55 +00002188def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002189 """upload the current branch to gerrit."""
2190 # We assume the remote called "origin" is the one we want.
2191 # It is probably not worthwhile to support different workflows.
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002192 gerrit_remote = 'origin'
luqui@chromium.org609f3952015-05-04 22:47:04 +00002193
2194 remote, remote_branch = cl.GetRemoteBranch()
2195 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2196 pending_prefix='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002197
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002198 change_desc = ChangeDescription(
2199 options.message or CreateDescriptionFromLog(args))
2200 if not change_desc.description:
andybons@chromium.org962f9462016-02-03 20:00:42 +00002201 print "\nDescription is empty. Aborting..."
2202 return 1
2203
2204 if options.title:
2205 print "\nPatch titles (-t) are not supported in Gerrit. Aborting..."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002206 return 1
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002207
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002208 if options.squash:
2209 # Try to get the message from a previous upload.
2210 shadow_branch = 'refs/heads/git_cl_uploads/' + cl.GetBranch()
bauerb@chromium.org13502e02016-02-18 10:18:29 +00002211 message = RunGitSilent(['show', '--format=%B', '-s', shadow_branch])
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002212 if not message:
2213 if not options.force:
2214 change_desc.prompt()
2215
2216 if CHANGE_ID not in change_desc.description:
2217 # Run the commit-msg hook without modifying the head commit by writing
2218 # the commit message to a temporary file and running the hook over it,
2219 # then reading the file back in.
2220 commit_msg_hook = os.path.join(settings.GetRoot(), '.git', 'hooks',
2221 'commit-msg')
2222 file_handle, msg_file = tempfile.mkstemp(text=True,
2223 prefix='commit_msg')
tandrii@chromium.orga83663a2016-01-14 16:01:00 +00002224 logging.debug("%s %s", file_handle, msg_file)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002225 try:
2226 try:
tandrii@chromium.orga83663a2016-01-14 16:01:00 +00002227 try:
2228 fileobj = os.fdopen(file_handle, 'w')
2229 except OSError:
2230 # if fdopen fails, file_handle remains open.
2231 # See https://docs.python.org/2/library/os.html#os.fdopen.
2232 os.close(file_handle)
2233 raise
2234 with fileobj:
2235 # This will close the file_handle.
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002236 fileobj.write(change_desc.description)
tandrii@chromium.orga83663a2016-01-14 16:01:00 +00002237 logging.debug("%s %s finish editing", file_handle, msg_file)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002238 finally:
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002239 RunCommand([commit_msg_hook, msg_file])
2240 change_desc.set_description(gclient_utils.FileRead(msg_file))
2241 finally:
2242 os.remove(msg_file)
2243
2244 if not change_desc.description:
2245 print "Description is empty; aborting."
2246 return 1
2247
2248 message = change_desc.description
2249
2250 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2251 if remote is '.':
2252 # If our upstream branch is local, we base our squashed commit on its
2253 # squashed version.
2254 parent = ('refs/heads/git_cl_uploads/' +
2255 scm.GIT.ShortBranchName(upstream_branch))
2256
2257 # Verify that the upstream branch has been uploaded too, otherwise Gerrit
2258 # will create additional CLs when uploading.
2259 if (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2260 RunGitSilent(['rev-parse', parent + ':'])):
2261 print 'Upload upstream branch ' + upstream_branch + ' first.'
2262 return 1
2263 else:
2264 parent = cl.GetCommonAncestorWithUpstream()
2265
2266 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2267 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2268 '-m', message]).strip()
2269 else:
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00002270 if not git_footers.get_footer_change_id(change_desc.description):
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002271 AddChangeIdToCommitMessage(options, args)
2272 ref_to_push = 'HEAD'
2273 parent = '%s/%s' % (gerrit_remote, branch)
2274
2275 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2276 ref_to_push)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002277 if len(commits) > 1:
2278 print('WARNING: This will upload %d commits. Run the following command '
2279 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002280 print('git log %s..%s' % (parent, ref_to_push))
2281 print('You can also use `git squash-branch` to squash these into a single '
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002282 'commit.')
2283 ask_for_data('About to upload; enter to confirm.')
2284
piman@chromium.org336f9122014-09-04 02:16:55 +00002285 if options.reviewers or options.tbr_owners:
2286 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002287
ukai@chromium.orge8077812012-02-03 03:41:46 +00002288 receive_options = []
2289 cc = cl.GetCCList().split(',')
2290 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002291 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002292 cc = filter(None, cc)
2293 if cc:
2294 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002295 if change_desc.get_reviewers():
2296 receive_options.extend(
2297 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002298
ukai@chromium.orge8077812012-02-03 03:41:46 +00002299 git_command = ['push']
2300 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00002301 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00002302 ' '.join(receive_options))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002303 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
ukai@chromium.orge8077812012-02-03 03:41:46 +00002304 RunGit(git_command)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002305
2306 if options.squash:
2307 head = RunGit(['rev-parse', 'HEAD']).strip()
2308 RunGit(['update-ref', '-m', 'Uploaded ' + head, shadow_branch, ref_to_push])
2309
ukai@chromium.orge8077812012-02-03 03:41:46 +00002310 # TODO(ukai): parse Change-Id: and set issue number?
2311 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002312
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002313
wittman@chromium.org455dc922015-01-26 20:15:50 +00002314def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
2315 """Computes the remote branch ref to use for the CL.
2316
2317 Args:
2318 remote (str): The git remote for the CL.
2319 remote_branch (str): The git remote branch for the CL.
2320 target_branch (str): The target branch specified by the user.
2321 pending_prefix (str): The pending prefix from the settings.
2322 """
2323 if not (remote and remote_branch):
2324 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002325
wittman@chromium.org455dc922015-01-26 20:15:50 +00002326 if target_branch:
2327 # Cannonicalize branch references to the equivalent local full symbolic
2328 # refs, which are then translated into the remote full symbolic refs
2329 # below.
2330 if '/' not in target_branch:
2331 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
2332 else:
2333 prefix_replacements = (
2334 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
2335 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
2336 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
2337 )
2338 match = None
2339 for regex, replacement in prefix_replacements:
2340 match = re.search(regex, target_branch)
2341 if match:
2342 remote_branch = target_branch.replace(match.group(0), replacement)
2343 break
2344 if not match:
2345 # This is a branch path but not one we recognize; use as-is.
2346 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00002347 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
2348 # Handle the refs that need to land in different refs.
2349 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002350
wittman@chromium.org455dc922015-01-26 20:15:50 +00002351 # Create the true path to the remote branch.
2352 # Does the following translation:
2353 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
2354 # * refs/remotes/origin/master -> refs/heads/master
2355 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
2356 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
2357 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
2358 elif remote_branch.startswith('refs/remotes/%s/' % remote):
2359 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
2360 'refs/heads/')
2361 elif remote_branch.startswith('refs/remotes/branch-heads'):
2362 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
2363 # If a pending prefix exists then replace refs/ with it.
2364 if pending_prefix:
2365 remote_branch = remote_branch.replace('refs/', pending_prefix)
2366 return remote_branch
2367
2368
piman@chromium.org336f9122014-09-04 02:16:55 +00002369def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002370 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002371 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2372 upload_args.extend(['--server', cl.GetRietveldServer()])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002373 upload_args.extend(auth.auth_config_to_command_options(cl.auth_config))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002374 if options.emulate_svn_auto_props:
2375 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002376
2377 change_desc = None
2378
pgervais@chromium.org91141372014-01-09 23:27:20 +00002379 if options.email is not None:
2380 upload_args.extend(['--email', options.email])
2381
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002382 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002383 if options.title:
2384 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00002385 if options.message:
2386 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00002387 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002388 print ("This branch is associated with issue %s. "
2389 "Adding patch to that issue." % cl.GetIssue())
2390 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002391 if options.title:
2392 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00002393 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002394 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00002395 if options.reviewers or options.tbr_owners:
2396 change_desc.update_reviewers(options.reviewers,
2397 options.tbr_owners,
2398 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002399 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002400 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002401
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002402 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002403 print "Description is empty; aborting."
2404 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002405
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002406 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002407 if change_desc.get_reviewers():
2408 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00002409 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002410 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00002411 DieWithError("Must specify reviewers to send email.")
2412 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00002413
2414 # We check this before applying rietveld.private assuming that in
2415 # rietveld.cc only addresses which we can send private CLs to are listed
2416 # if rietveld.private is set, and so we should ignore rietveld.cc only when
2417 # --private is specified explicitly on the command line.
2418 if options.private:
2419 logging.warn('rietveld.cc is ignored since private flag is specified. '
2420 'You need to review and add them manually if necessary.')
2421 cc = cl.GetCCListWithoutDefault()
2422 else:
2423 cc = cl.GetCCList()
2424 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00002425 if cc:
2426 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002427
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002428 if options.private or settings.GetDefaultPrivateFlag() == "True":
2429 upload_args.append('--private')
2430
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002431 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00002432 if not options.find_copies:
2433 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002434
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002435 # Include the upstream repo's URL in the change -- this is useful for
2436 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002437 remote_url = cl.GetGitBaseUrlFromConfig()
2438 if not remote_url:
2439 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002440 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002441 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00002442 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
2443 remote_url = (cl.GetRemoteUrl() + '@'
2444 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002445 if remote_url:
2446 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00002447 remote, remote_branch = cl.GetRemoteBranch()
wittman@chromium.org455dc922015-01-26 20:15:50 +00002448 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2449 settings.GetPendingRefPrefix())
2450 if target_ref:
2451 upload_args.extend(['--target_ref', target_ref])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002452
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002453 # Look for dependent patchsets. See crbug.com/480453 for more details.
2454 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2455 upstream_branch = ShortBranchName(upstream_branch)
2456 if remote is '.':
2457 # A local branch is being tracked.
2458 local_branch = ShortBranchName(upstream_branch)
rmistry@google.com78948ed2015-07-08 23:09:57 +00002459 if settings.GetIsSkipDependencyUpload(local_branch):
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002460 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002461 print ('Skipping dependency patchset upload because git config '
2462 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002463 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002464 else:
2465 auth_config = auth.extract_auth_config_from_options(options)
2466 branch_cl = Changelist(branchref=local_branch, auth_config=auth_config)
2467 branch_cl_issue_url = branch_cl.GetIssueURL()
2468 branch_cl_issue = branch_cl.GetIssue()
2469 branch_cl_patchset = branch_cl.GetPatchset()
2470 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2471 upload_args.extend(
2472 ['--depends_on_patchset', '%s:%s' % (
2473 branch_cl_issue, branch_cl_patchset)])
2474 print
2475 print ('The current branch (%s) is tracking a local branch (%s) with '
2476 'an associated CL.') % (cl.GetBranch(), local_branch)
2477 print 'Adding %s/#ps%s as a dependency patchset.' % (
2478 branch_cl_issue_url, branch_cl_patchset)
2479 print
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002480
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002481 project = settings.GetProject()
2482 if project:
2483 upload_args.extend(['--project', project])
2484
rmistry@google.comef966222015-04-07 11:15:01 +00002485 if options.cq_dry_run:
2486 upload_args.extend(['--cq_dry_run'])
2487
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002488 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00002489 upload_args = ['upload'] + upload_args + args
2490 logging.info('upload.RealMain(%s)', upload_args)
2491 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00002492 issue = int(issue)
2493 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00002494 except KeyboardInterrupt:
2495 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002496 except:
2497 # If we got an exception after the user typed a description for their
2498 # change, back up the description before re-raising.
2499 if change_desc:
2500 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2501 print '\nGot exception while uploading -- saving description to %s\n' \
2502 % backup_path
2503 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002504 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002505 backup_file.close()
2506 raise
2507
2508 if not cl.GetIssue():
2509 cl.SetIssue(issue)
2510 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002511
2512 if options.use_commit_queue:
2513 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002514 return 0
2515
2516
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002517def cleanup_list(l):
2518 """Fixes a list so that comma separated items are put as individual items.
2519
2520 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
2521 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
2522 """
2523 items = sum((i.split(',') for i in l), [])
2524 stripped_items = (i.strip() for i in items)
2525 return sorted(filter(None, stripped_items))
2526
2527
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002528@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002529def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00002530 """Uploads the current changelist to codereview.
2531
2532 Can skip dependency patchset uploads for a branch by running:
2533 git config branch.branch_name.skip-deps-uploads True
2534 To unset run:
2535 git config --unset branch.branch_name.skip-deps-uploads
2536 Can also set the above globally by using the --global flag.
2537 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00002538 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2539 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002540 parser.add_option('--bypass-watchlists', action='store_true',
2541 dest='bypass_watchlists',
2542 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002543 parser.add_option('-f', action='store_true', dest='force',
2544 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002545 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00002546 parser.add_option('-t', dest='title',
2547 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002548 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002549 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002550 help='reviewer email addresses')
2551 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002552 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002553 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00002554 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00002555 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002556 parser.add_option('--emulate_svn_auto_props',
2557 '--emulate-svn-auto-props',
2558 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00002559 dest="emulate_svn_auto_props",
2560 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00002561 parser.add_option('-c', '--use-commit-queue', action='store_true',
2562 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002563 parser.add_option('--private', action='store_true',
2564 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00002565 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002566 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00002567 metavar='TARGET',
2568 help='Apply CL to remote ref TARGET. ' +
2569 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002570 parser.add_option('--squash', action='store_true',
2571 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002572 parser.add_option('--no-squash', action='store_true',
2573 help='Don\'t squash multiple commits into one ' +
2574 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002575 parser.add_option('--email', default=None,
2576 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00002577 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
2578 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00002579 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
2580 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00002581 help='Send the patchset to do a CQ dry run right after '
2582 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00002583 parser.add_option('--dependencies', action='store_true',
2584 help='Uploads CLs of all the local branches that depend on '
2585 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002586
rmistry@google.com2dd99862015-06-22 12:22:18 +00002587 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002588 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002589 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002590 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002591 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002592
sbc@chromium.org71437c02015-04-09 19:29:40 +00002593 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002594 return 1
2595
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002596 options.reviewers = cleanup_list(options.reviewers)
2597 options.cc = cleanup_list(options.cc)
2598
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002599 cl = Changelist(auth_config=auth_config)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002600 if args:
2601 # TODO(ukai): is it ok for gerrit case?
2602 base_branch = args[0]
2603 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00002604 if cl.GetBranch() is None:
2605 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
2606
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002607 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002608 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00002609 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00002610
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00002611 # Make sure authenticated to Rietveld before running expensive hooks. It is
2612 # a fast, best efforts check. Rietveld still can reject the authentication
2613 # during the actual upload.
2614 if not settings.GetIsGerrit() and auth_config.use_oauth2:
2615 authenticator = auth.get_authenticator_for_host(
2616 cl.GetRietveldServer(), auth_config)
2617 if not authenticator.has_cached_credentials():
2618 raise auth.LoginRequiredError(cl.GetRietveldServer())
2619
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002620 # Apply watchlists on upload.
2621 change = cl.GetChange(base_branch, None)
2622 watchlist = watchlists.Watchlists(change.RepositoryRoot())
2623 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002624 if not options.bypass_watchlists:
2625 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002626
ukai@chromium.orge8077812012-02-03 03:41:46 +00002627 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00002628 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002629 # Set the reviewer list now so that presubmit checks can access it.
2630 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00002631 change_description.update_reviewers(options.reviewers,
2632 options.tbr_owners,
2633 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002634 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002635 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00002636 may_prompt=not options.force,
2637 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002638 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002639 if not hook_results.should_continue():
2640 return 1
2641 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002642 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002643
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002644 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002645 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002646 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00002647 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002648 print ('The last upload made from this repository was patchset #%d but '
2649 'the most recent patchset on the server is #%d.'
2650 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00002651 print ('Uploading will still work, but if you\'ve uploaded to this issue '
2652 'from another machine or branch the patch you\'re uploading now '
2653 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002654 ask_for_data('About to upload; enter to confirm.')
2655
iannucci@chromium.org79540052012-10-19 23:15:26 +00002656 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002657 if settings.GetIsGerrit():
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002658 if options.squash and options.no_squash:
2659 DieWithError('Can only use one of --squash or --no-squash')
2660
2661 options.squash = ((settings.GetSquashGerritUploads() or options.squash) and
2662 not options.no_squash)
2663
tandrii@chromium.org1e67bb72016-02-11 12:15:49 +00002664 ret = GerritUpload(options, args, cl, change)
2665 else:
2666 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002667 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00002668 git_set_branch_value('last-upload-hash',
2669 RunGit(['rev-parse', 'HEAD']).strip())
rmistry@google.com5626a922015-02-26 14:03:30 +00002670 # Run post upload hooks, if specified.
2671 if settings.GetRunPostUploadHook():
2672 presubmit_support.DoPostUploadExecuter(
2673 change,
2674 cl,
2675 settings.GetRoot(),
2676 options.verbose,
2677 sys.stdout)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002678
rmistry@google.com2dd99862015-06-22 12:22:18 +00002679 # Upload all dependencies if specified.
2680 if options.dependencies:
2681 print
2682 print '--dependencies has been specified.'
2683 print 'All dependent local branches will be re-uploaded.'
2684 print
2685 # Remove the dependencies flag from args so that we do not end up in a
2686 # loop.
2687 orig_args.remove('--dependencies')
2688 upload_branch_deps(cl, orig_args)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002689 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00002690
2691
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002692def IsSubmoduleMergeCommit(ref):
2693 # When submodules are added to the repo, we expect there to be a single
2694 # non-git-svn merge commit at remote HEAD with a signature comment.
2695 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00002696 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002697 return RunGit(cmd) != ''
2698
2699
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002700def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002701 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002702
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002703 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002704 Updates changelog with metadata (e.g. pointer to review).
2705 Pushes/dcommits the code upstream.
2706 Updates review and closes.
2707 """
2708 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2709 help='bypass upload presubmit hook')
2710 parser.add_option('-m', dest='message',
2711 help="override review description")
2712 parser.add_option('-f', action='store_true', dest='force',
2713 help="force yes to questions (don't prompt)")
2714 parser.add_option('-c', dest='contributor',
2715 help="external contributor for patch (appended to " +
2716 "description and used as author for git). Should be " +
2717 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002718 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002719 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002720 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002721 auth_config = auth.extract_auth_config_from_options(options)
2722
2723 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002724
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002725 current = cl.GetBranch()
2726 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2727 if not settings.GetIsGitSvn() and remote == '.':
2728 print
2729 print 'Attempting to push branch %r into another local branch!' % current
2730 print
2731 print 'Either reparent this branch on top of origin/master:'
2732 print ' git reparent-branch --root'
2733 print
2734 print 'OR run `git rebase-update` if you think the parent branch is already'
2735 print 'committed.'
2736 print
2737 print ' Current parent: %r' % upstream_branch
2738 return 1
2739
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002740 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002741 # Default to merging against our best guess of the upstream branch.
2742 args = [cl.GetUpstreamBranch()]
2743
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002744 if options.contributor:
2745 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
2746 print "Please provide contibutor as 'First Last <email@example.com>'"
2747 return 1
2748
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002749 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002750 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002751
sbc@chromium.org71437c02015-04-09 19:29:40 +00002752 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002753 return 1
2754
2755 # This rev-list syntax means "show all commits not in my branch that
2756 # are in base_branch".
2757 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
2758 base_branch]).splitlines()
2759 if upstream_commits:
2760 print ('Base branch "%s" has %d commits '
2761 'not in this branch.' % (base_branch, len(upstream_commits)))
2762 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
2763 return 1
2764
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002765 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002766 svn_head = None
2767 if cmd == 'dcommit' or base_has_submodules:
2768 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
2769 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002770
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002771 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002772 # If the base_head is a submodule merge commit, the first parent of the
2773 # base_head should be a git-svn commit, which is what we're interested in.
2774 base_svn_head = base_branch
2775 if base_has_submodules:
2776 base_svn_head += '^1'
2777
2778 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002779 if extra_commits:
2780 print ('This branch has %d additional commits not upstreamed yet.'
2781 % len(extra_commits.splitlines()))
2782 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
2783 'before attempting to %s.' % (base_branch, cmd))
2784 return 1
2785
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002786 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002787 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002788 author = None
2789 if options.contributor:
2790 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002791 hook_results = cl.RunHook(
2792 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002793 may_prompt=not options.force,
2794 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002795 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002796 if not hook_results.should_continue():
2797 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002798
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002799 # Check the tree status if the tree status URL is set.
2800 status = GetTreeStatus()
2801 if 'closed' == status:
2802 print('The tree is closed. Please wait for it to reopen. Use '
2803 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2804 return 1
2805 elif 'unknown' == status:
2806 print('Unable to determine tree status. Please verify manually and '
2807 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2808 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002809
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002810 change_desc = ChangeDescription(options.message)
2811 if not change_desc.description and cl.GetIssue():
2812 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002813
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002814 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00002815 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002816 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00002817 else:
2818 print 'No description set.'
2819 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
2820 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002821
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002822 # Keep a separate copy for the commit message, because the commit message
2823 # contains the link to the Rietveld issue, while the Rietveld message contains
2824 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002825 # Keep a separate copy for the commit message.
2826 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00002827 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002828
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002829 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00002830 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00002831 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00002832 # after it. Add a period on a new line to circumvent this. Also add a space
2833 # before the period to make sure that Gitiles continues to correctly resolve
2834 # the URL.
2835 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002836 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002837 commit_desc.append_footer('Patch from %s.' % options.contributor)
2838
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00002839 print('Description:')
2840 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002841
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002842 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002843 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00002844 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002845
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002846 # We want to squash all this branch's commits into one commit with the proper
2847 # description. We do this by doing a "reset --soft" to the base branch (which
2848 # keeps the working copy the same), then dcommitting that. If origin/master
2849 # has a submodule merge commit, we'll also need to cherry-pick the squashed
2850 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002851 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002852 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
2853 # Delete the branches if they exist.
2854 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
2855 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
2856 result = RunGitWithCode(showref_cmd)
2857 if result[0] == 0:
2858 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002859
2860 # We might be in a directory that's present in this branch but not in the
2861 # trunk. Move up to the top of the tree so that git commands that expect a
2862 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002863 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002864 if rel_base_path:
2865 os.chdir(rel_base_path)
2866
2867 # Stuff our change into the merge branch.
2868 # We wrap in a try...finally block so if anything goes wrong,
2869 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002870 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002871 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002872 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002873 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002874 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00002875 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002876 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002877 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002878 RunGit(
2879 [
2880 'commit', '--author', options.contributor,
2881 '-m', commit_desc.description,
2882 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002883 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002884 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002885 if base_has_submodules:
2886 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
2887 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
2888 RunGit(['checkout', CHERRY_PICK_BRANCH])
2889 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002890 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002891 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002892 pending_prefix = settings.GetPendingRefPrefix()
2893 if not pending_prefix or branch.startswith(pending_prefix):
2894 # If not using refs/pending/heads/* at all, or target ref is already set
2895 # to pending, then push to the target ref directly.
2896 retcode, output = RunGitWithCode(
2897 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002898 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002899 else:
2900 # Cherry-pick the change on top of pending ref and then push it.
2901 assert branch.startswith('refs/'), branch
2902 assert pending_prefix[-1] == '/', pending_prefix
2903 pending_ref = pending_prefix + branch[len('refs/'):]
2904 retcode, output = PushToGitPending(remote, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002905 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002906 if retcode == 0:
2907 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002908 else:
2909 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002910 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002911 'svn', 'dcommit',
2912 '-C%s' % options.similarity,
2913 '--no-rebase', '--rmdir',
2914 ]
2915 if settings.GetForceHttpsCommitUrl():
2916 # Allow forcing https commit URLs for some projects that don't allow
2917 # committing to http URLs (like Google Code).
2918 remote_url = cl.GetGitSvnRemoteUrl()
2919 if urlparse.urlparse(remote_url).scheme == 'http':
2920 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002921 cmd_args.append('--commit-url=%s' % remote_url)
2922 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002923 if 'Committed r' in output:
2924 revision = re.match(
2925 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2926 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002927 finally:
2928 # And then swap back to the original branch and clean up.
2929 RunGit(['checkout', '-q', cl.GetBranch()])
2930 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002931 if base_has_submodules:
2932 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002933
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002934 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002935 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002936 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002937
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002938 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002939 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002940 try:
2941 revision = WaitForRealCommit(remote, revision, base_branch, branch)
2942 # We set pushed_to_pending to False, since it made it all the way to the
2943 # real ref.
2944 pushed_to_pending = False
2945 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002946 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002947
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002948 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002949 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002950 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002951 if not to_pending:
2952 if viewvc_url and revision:
2953 change_desc.append_footer(
2954 'Committed: %s%s' % (viewvc_url, revision))
2955 elif revision:
2956 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002957 print ('Closing issue '
2958 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002959 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002960 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002961 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002962 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00002963 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00002964 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002965 if options.bypass_hooks:
2966 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2967 else:
2968 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002969 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002970 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002971
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002972 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002973 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
2974 print 'The commit is in the pending queue (%s).' % pending_ref
2975 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00002976 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002977 'footer.' % branch)
2978
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002979 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2980 if os.path.isfile(hook):
2981 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002982
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002983 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002984
2985
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002986def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
2987 print
2988 print 'Waiting for commit to be landed on %s...' % real_ref
2989 print '(If you are impatient, you may Ctrl-C once without harm)'
2990 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
2991 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
2992
2993 loop = 0
2994 while True:
2995 sys.stdout.write('fetching (%d)... \r' % loop)
2996 sys.stdout.flush()
2997 loop += 1
2998
2999 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
3000 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
3001 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
3002 for commit in commits.splitlines():
3003 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
3004 print 'Found commit on %s' % real_ref
3005 return commit
3006
3007 current_rev = to_rev
3008
3009
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003010def PushToGitPending(remote, pending_ref, upstream_ref):
3011 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
3012
3013 Returns:
3014 (retcode of last operation, output log of last operation).
3015 """
3016 assert pending_ref.startswith('refs/'), pending_ref
3017 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
3018 cherry = RunGit(['rev-parse', 'HEAD']).strip()
3019 code = 0
3020 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003021 max_attempts = 3
3022 attempts_left = max_attempts
3023 while attempts_left:
3024 if attempts_left != max_attempts:
3025 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
3026 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003027
3028 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003029 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003030 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003031 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003032 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003033 print 'Fetch failed with exit code %d.' % code
3034 if out.strip():
3035 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003036 continue
3037
3038 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003039 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003040 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003041 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003042 if code:
3043 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003044 'Your patch doesn\'t apply cleanly to ref \'%s\', '
3045 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003046 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
3047 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003048 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003049 return code, out
3050
3051 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003052 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003053 code, out = RunGitWithCode(
3054 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
3055 if code == 0:
3056 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003057 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003058 return code, out
3059
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003060 print 'Push failed with exit code %d.' % code
3061 if out.strip():
3062 print out.strip()
3063 if IsFatalPushFailure(out):
3064 print (
3065 'Fatal push error. Make sure your .netrc credentials and git '
3066 'user.email are correct and you have push access to the repo.')
3067 return code, out
3068
3069 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003070 return code, out
3071
3072
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003073def IsFatalPushFailure(push_stdout):
3074 """True if retrying push won't help."""
3075 return '(prohibited by Gerrit)' in push_stdout
3076
3077
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003078@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003079def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003080 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003081 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003082 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003083 # If it looks like previous commits were mirrored with git-svn.
3084 message = """This repository appears to be a git-svn mirror, but no
3085upstream SVN master is set. You probably need to run 'git auto-svn' once."""
3086 else:
3087 message = """This doesn't appear to be an SVN repository.
3088If your project has a true, writeable git repository, you probably want to run
3089'git cl land' instead.
3090If your project has a git mirror of an upstream SVN master, you probably need
3091to run 'git svn init'.
3092
3093Using the wrong command might cause your commit to appear to succeed, and the
3094review to be closed, without actually landing upstream. If you choose to
3095proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00003096 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00003097 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003098 return SendUpstream(parser, args, 'dcommit')
3099
3100
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003101@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003102def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003103 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003104 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003105 print('This appears to be an SVN repository.')
3106 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003107 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00003108 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003109 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003110
3111
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003112def ParseIssueNum(arg):
3113 """Parses the issue number from args if present otherwise returns None."""
3114 if re.match(r'\d+', arg):
3115 return arg
3116 if arg.startswith('http'):
3117 return re.sub(r'.*/(\d+)/?', r'\1', arg)
3118 return None
3119
3120
3121@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003122def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00003123 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003124 parser.add_option('-b', dest='newbranch',
3125 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003126 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003127 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003128 parser.add_option('-d', '--directory', action='store', metavar='DIR',
3129 help='Change to the directory DIR immediately, '
3130 'before doing anything else.')
3131 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003132 help='failed patches spew .rej files rather than '
3133 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003134 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
3135 help="don't commit after patch applies")
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003136
3137 group = optparse.OptionGroup(parser,
3138 """Options for continuing work on the current issue uploaded
3139from a different clone (e.g. different machine). Must be used independently from
3140the other options. No issue number should be specified, and the branch must have
3141an issue number associated with it""")
3142 group.add_option('--reapply', action='store_true',
3143 dest='reapply',
3144 help="""Reset the branch and reapply the issue.
3145CAUTION: This will undo any local changes in this branch""")
3146
3147 group.add_option('--pull', action='store_true', dest='pull',
3148 help="Performs a pull before reapplying.")
3149 parser.add_option_group(group)
3150
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003151 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003152 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003153 auth_config = auth.extract_auth_config_from_options(options)
3154
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003155 issue_arg = None
3156 if options.reapply :
3157 if len(args) > 0:
3158 parser.error("--reapply implies no additional arguments.")
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003159
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003160 cl = Changelist()
3161 issue_arg = cl.GetIssue()
3162 upstream = cl.GetUpstreamBranch()
3163 if upstream == None:
3164 parser.error("No upstream branch specified. Cannot reset branch")
3165
3166 RunGit(['reset', '--hard', upstream])
3167 if options.pull:
3168 RunGit(['pull'])
3169 else:
3170 if len(args) != 1:
3171 parser.error("Must specify issue number")
3172
3173 issue_arg = ParseIssueNum(args[0])
3174
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003175 # The patch URL works because ParseIssueNum won't do any substitution
3176 # as the re.sub pattern fails to match and just returns it.
3177 if issue_arg == None:
3178 parser.print_help()
3179 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003180
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003181 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003182 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003183 return 1
3184
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003185 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00003186 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003187
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003188 if options.newbranch:
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003189 if options.reapply:
3190 parser.error("--reapply excludes any option other than --pull")
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003191 if options.force:
3192 RunGit(['branch', '-D', options.newbranch],
3193 stderr=subprocess2.PIPE, error_ok=True)
3194 RunGit(['checkout', '-b', options.newbranch,
3195 Changelist().GetUpstreamBranch()])
3196
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003197 return PatchIssue(issue_arg, options.reject, options.nocommit,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003198 options.directory, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003199
3200
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003201def PatchIssue(issue_arg, reject, nocommit, directory, auth_config):
wychen@chromium.orga872e752015-04-28 23:42:18 +00003202 # PatchIssue should never be called with a dirty tree. It is up to the
3203 # caller to check this, but just in case we assert here since the
3204 # consequences of the caller not checking this could be dire.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003205 assert(not git_common.is_dirty_git_tree('apply'))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003206
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003207 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003208 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00003209 issue = int(issue_arg)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003210 cl = Changelist(issue=issue, auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003211 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00003212 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003213 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00003214 # Assume it's a URL to the patch. Default to https.
3215 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
kjellander@chromium.org44424542015-06-02 18:35:29 +00003216 match = re.match(r'(.*?)/download/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00003217 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003218 DieWithError('Must pass an issue ID or full URL for '
3219 '\'Download raw patch set\'')
kjellander@chromium.org44424542015-06-02 18:35:29 +00003220 issue = int(match.group(2))
3221 cl = Changelist(issue=issue, auth_config=auth_config)
3222 cl.rietveld_server = match.group(1)
3223 patchset = int(match.group(3))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00003224 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003225
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003226 # Switch up to the top-level directory, if necessary, in preparation for
3227 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003228 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003229 if top:
3230 os.chdir(top)
3231
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003232 # Git patches have a/ at the beginning of source paths. We strip that out
3233 # with a sed script rather than the -p flag to patch so we can feed either
3234 # Git or svn-style patches into the same apply command.
3235 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003236 try:
3237 patch_data = subprocess2.check_output(
3238 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
3239 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003240 DieWithError('Git patch mungling failed.')
3241 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003242
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003243 # We use "git apply" to apply the patch instead of "patch" so that we can
3244 # pick up file adds.
3245 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003246 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003247 if directory:
3248 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003249 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003250 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003251 elif IsGitVersionAtLeast('1.7.12'):
3252 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003253 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003254 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003255 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003256 except subprocess2.CalledProcessError:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003257 print 'Failed to apply the patch'
3258 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003259
3260 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003261 if not nocommit:
wychen@chromium.org5b3bebb2015-05-28 21:41:43 +00003262 RunGit(['commit', '-m', (cl.GetDescription() + '\n\n' +
3263 'patch from issue %(i)s at patchset '
carlosk@chromium.org71284d92014-11-14 18:12:50 +00003264 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
3265 % {'i': issue, 'p': patchset})])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003266 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003267 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00003268 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00003269 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003270 else:
3271 print "Patch applied to index."
3272 return 0
3273
3274
3275def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003276 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003277 # Provide a wrapper for git svn rebase to help avoid accidental
3278 # git svn dcommit.
3279 # It's the only command that doesn't use parser at all since we just defer
3280 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003281
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003282 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003283
3284
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003285def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003286 """Fetches the tree status and returns either 'open', 'closed',
3287 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003288 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003289 if url:
3290 status = urllib2.urlopen(url).read().lower()
3291 if status.find('closed') != -1 or status == '0':
3292 return 'closed'
3293 elif status.find('open') != -1 or status == '1':
3294 return 'open'
3295 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003296 return 'unset'
3297
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003298
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003299def GetTreeStatusReason():
3300 """Fetches the tree status from a json url and returns the message
3301 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00003302 url = settings.GetTreeStatusUrl()
3303 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003304 connection = urllib2.urlopen(json_url)
3305 status = json.loads(connection.read())
3306 connection.close()
3307 return status['message']
3308
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003309
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003310def GetBuilderMaster(bot_list):
3311 """For a given builder, fetch the master from AE if available."""
3312 map_url = 'https://builders-map.appspot.com/'
3313 try:
3314 master_map = json.load(urllib2.urlopen(map_url))
3315 except urllib2.URLError as e:
3316 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
3317 (map_url, e))
3318 except ValueError as e:
3319 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
3320 if not master_map:
3321 return None, 'Failed to build master map.'
3322
3323 result_master = ''
3324 for bot in bot_list:
3325 builder = bot.split(':', 1)[0]
3326 master_list = master_map.get(builder, [])
3327 if not master_list:
3328 return None, ('No matching master for builder %s.' % builder)
3329 elif len(master_list) > 1:
3330 return None, ('The builder name %s exists in multiple masters %s.' %
3331 (builder, master_list))
3332 else:
3333 cur_master = master_list[0]
3334 if not result_master:
3335 result_master = cur_master
3336 elif result_master != cur_master:
3337 return None, 'The builders do not belong to the same master.'
3338 return result_master, None
3339
3340
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003341def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003342 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003343 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003344 status = GetTreeStatus()
3345 if 'unset' == status:
3346 print 'You must configure your tree status URL by running "git cl config".'
3347 return 2
3348
3349 print "The tree is %s" % status
3350 print
3351 print GetTreeStatusReason()
3352 if status != 'open':
3353 return 1
3354 return 0
3355
3356
maruel@chromium.org15192402012-09-06 12:38:29 +00003357def CMDtry(parser, args):
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003358 """Triggers a try job through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00003359 group = optparse.OptionGroup(parser, "Try job options")
3360 group.add_option(
3361 "-b", "--bot", action="append",
3362 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
3363 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003364 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00003365 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003366 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00003367 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003368 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00003369 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003370 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003371 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003372 "-r", "--revision",
3373 help="Revision to use for the try job; default: the "
3374 "revision will be determined by the try server; see "
3375 "its waterfall for more info")
3376 group.add_option(
3377 "-c", "--clobber", action="store_true", default=False,
3378 help="Force a clobber before building; e.g. don't do an "
3379 "incremental build")
3380 group.add_option(
3381 "--project",
3382 help="Override which project to use. Projects are defined "
3383 "server-side to define what default bot set to use")
3384 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00003385 "-p", "--property", dest="properties", action="append", default=[],
3386 help="Specify generic properties in the form -p key1=value1 -p "
3387 "key2=value2 etc (buildbucket only). The value will be treated as "
3388 "json if decodable, or as string otherwise.")
3389 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003390 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003391 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003392 "--use-rietveld", action="store_true", default=False,
3393 help="Use Rietveld to trigger try jobs.")
3394 group.add_option(
3395 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3396 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00003397 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003398 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00003399 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003400 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00003401
machenbach@chromium.org45453142015-09-15 08:45:22 +00003402 if options.use_rietveld and options.properties:
3403 parser.error('Properties can only be specified with buildbucket')
3404
3405 # Make sure that all properties are prop=value pairs.
3406 bad_params = [x for x in options.properties if '=' not in x]
3407 if bad_params:
3408 parser.error('Got properties with missing "=": %s' % bad_params)
3409
maruel@chromium.org15192402012-09-06 12:38:29 +00003410 if args:
3411 parser.error('Unknown arguments: %s' % args)
3412
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003413 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00003414 if not cl.GetIssue():
3415 parser.error('Need to upload first')
3416
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003417 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00003418 if props.get('closed'):
3419 parser.error('Cannot send tryjobs for a closed CL')
3420
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003421 if props.get('private'):
3422 parser.error('Cannot use trybots with private issue')
3423
maruel@chromium.org15192402012-09-06 12:38:29 +00003424 if not options.name:
3425 options.name = cl.GetBranch()
3426
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003427 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003428 options.master, err_msg = GetBuilderMaster(options.bot)
3429 if err_msg:
3430 parser.error('Tryserver master cannot be found because: %s\n'
3431 'Please manually specify the tryserver master'
3432 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003433
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003434 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003435 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003436 if not options.bot:
3437 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00003438
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003439 # Get try masters from PRESUBMIT.py files.
3440 masters = presubmit_support.DoGetTryMasters(
3441 change,
3442 change.LocalPaths(),
3443 settings.GetRoot(),
3444 None,
3445 None,
3446 options.verbose,
3447 sys.stdout)
3448 if masters:
3449 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00003450
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003451 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
3452 options.bot = presubmit_support.DoGetTrySlaves(
3453 change,
3454 change.LocalPaths(),
3455 settings.GetRoot(),
3456 None,
3457 None,
3458 options.verbose,
3459 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00003460
3461 if not options.bot:
3462 # Get try masters from cq.cfg if any.
3463 # TODO(tandrii): some (but very few) projects store cq.cfg in different
3464 # location.
3465 cq_cfg = os.path.join(change.RepositoryRoot(),
3466 'infra', 'config', 'cq.cfg')
3467 if os.path.exists(cq_cfg):
3468 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00003469 cq_masters = commit_queue.get_master_builder_map(
3470 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00003471 for master, builders in cq_masters.iteritems():
3472 for builder in builders:
3473 # Skip presubmit builders, because these will fail without LGTM.
3474 if 'presubmit' not in builder.lower():
3475 masters.setdefault(master, {})[builder] = ['defaulttests']
3476 if masters:
3477 return masters
3478
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003479 if not options.bot:
3480 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00003481
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003482 builders_and_tests = {}
3483 # TODO(machenbach): The old style command-line options don't support
3484 # multiple try masters yet.
3485 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
3486 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
3487
3488 for bot in old_style:
3489 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003490 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003491 elif ',' in bot:
3492 parser.error('Specify one bot per --bot flag')
3493 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00003494 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003495
3496 for bot, tests in new_style:
3497 builders_and_tests.setdefault(bot, []).extend(tests)
3498
3499 # Return a master map with one master to be backwards compatible. The
3500 # master name defaults to an empty string, which will cause the master
3501 # not to be set on rietveld (deprecated).
3502 return {options.master: builders_and_tests}
3503
3504 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00003505
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003506 for builders in masters.itervalues():
3507 if any('triggered' in b for b in builders):
3508 print >> sys.stderr, (
3509 'ERROR You are trying to send a job to a triggered bot. This type of'
3510 ' bot requires an\ninitial job from a parent (usually a builder). '
3511 'Instead send your job to the parent.\n'
3512 'Bot list: %s' % builders)
3513 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00003514
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00003515 patchset = cl.GetMostRecentPatchset()
3516 if patchset and patchset != cl.GetPatchset():
3517 print(
3518 '\nWARNING Mismatch between local config and server. Did a previous '
3519 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3520 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003521 if options.luci:
3522 trigger_luci_job(cl, masters, options)
3523 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003524 try:
3525 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
3526 except BuildbucketResponseException as ex:
3527 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00003528 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003529 except Exception as e:
3530 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3531 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
3532 e, stacktrace)
3533 return 1
3534 else:
3535 try:
3536 cl.RpcServer().trigger_distributed_try_jobs(
3537 cl.GetIssue(), patchset, options.name, options.clobber,
3538 options.revision, masters)
3539 except urllib2.HTTPError as e:
3540 if e.code == 404:
3541 print('404 from rietveld; '
3542 'did you mean to use "git try" instead of "git cl try"?')
3543 return 1
3544 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003545
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003546 for (master, builders) in sorted(masters.iteritems()):
3547 if master:
3548 print 'Master: %s' % master
3549 length = max(len(builder) for builder in builders)
3550 for builder in sorted(builders):
3551 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00003552 return 0
3553
3554
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00003555def CMDtry_results(parser, args):
3556 group = optparse.OptionGroup(parser, "Try job results options")
3557 group.add_option(
3558 "-p", "--patchset", type=int, help="patchset number if not current.")
3559 group.add_option(
3560 "--print-master", action='store_true', help="print master name as well")
3561 group.add_option(
3562 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3563 help="Host of buildbucket. The default host is %default.")
3564 parser.add_option_group(group)
3565 auth.add_auth_options(parser)
3566 options, args = parser.parse_args(args)
3567 if args:
3568 parser.error('Unrecognized args: %s' % ' '.join(args))
3569
3570 auth_config = auth.extract_auth_config_from_options(options)
3571 cl = Changelist(auth_config=auth_config)
3572 if not cl.GetIssue():
3573 parser.error('Need to upload first')
3574
3575 if not options.patchset:
3576 options.patchset = cl.GetMostRecentPatchset()
3577 if options.patchset and options.patchset != cl.GetPatchset():
3578 print(
3579 '\nWARNING Mismatch between local config and server. Did a previous '
3580 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3581 'Continuing using\npatchset %s.\n' % options.patchset)
3582 try:
3583 jobs = fetch_try_jobs(auth_config, cl, options)
3584 except BuildbucketResponseException as ex:
3585 print 'Buildbucket error: %s' % ex
3586 return 1
3587 except Exception as e:
3588 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3589 print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
3590 e, stacktrace)
3591 return 1
3592 print_tryjobs(options, jobs)
3593 return 0
3594
3595
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003596@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003597def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003598 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003599 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003600 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003601 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003602
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003603 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003604 if args:
3605 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003606 branch = cl.GetBranch()
3607 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003608 cl = Changelist()
3609 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003610
3611 # Clear configured merge-base, if there is one.
3612 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003613 else:
3614 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003615 return 0
3616
3617
thestig@chromium.org00858c82013-12-02 23:08:03 +00003618def CMDweb(parser, args):
3619 """Opens the current CL in the web browser."""
3620 _, args = parser.parse_args(args)
3621 if args:
3622 parser.error('Unrecognized args: %s' % ' '.join(args))
3623
3624 issue_url = Changelist().GetIssueURL()
3625 if not issue_url:
3626 print >> sys.stderr, 'ERROR No issue to open'
3627 return 1
3628
3629 webbrowser.open(issue_url)
3630 return 0
3631
3632
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003633def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003634 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003635 auth.add_auth_options(parser)
3636 options, args = parser.parse_args(args)
3637 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003638 if args:
3639 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003640 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003641 props = cl.GetIssueProperties()
3642 if props.get('private'):
3643 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003644 cl.SetFlag('commit', '1')
3645 return 0
3646
3647
groby@chromium.org411034a2013-02-26 15:12:01 +00003648def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003649 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003650 auth.add_auth_options(parser)
3651 options, args = parser.parse_args(args)
3652 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00003653 if args:
3654 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003655 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00003656 # Ensure there actually is an issue to close.
3657 cl.GetDescription()
3658 cl.CloseIssue()
3659 return 0
3660
3661
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003662def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003663 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003664 auth.add_auth_options(parser)
3665 options, args = parser.parse_args(args)
3666 auth_config = auth.extract_auth_config_from_options(options)
3667 if args:
3668 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003669
3670 # Uncommitted (staged and unstaged) changes will be destroyed by
3671 # "git reset --hard" if there are merging conflicts in PatchIssue().
3672 # Staged changes would be committed along with the patch from last
3673 # upload, hence counted toward the "last upload" side in the final
3674 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003675 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003676 return 1
3677
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003678 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003679 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003680 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003681 if not issue:
3682 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003683 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003684 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003685
3686 # Create a new branch based on the merge-base
3687 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
3688 try:
3689 # Patch in the latest changes from rietveld.
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003690 rtn = PatchIssue(issue, False, False, None, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003691 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003692 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003693 return rtn
3694
wychen@chromium.org06928532015-02-03 02:11:29 +00003695 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003696 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00003697 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003698 finally:
3699 RunGit(['checkout', '-q', branch])
3700 RunGit(['branch', '-D', TMP_BRANCH])
3701
3702 return 0
3703
3704
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003705def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003706 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003707 parser.add_option(
3708 '--no-color',
3709 action='store_true',
3710 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003711 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003712 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003713 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003714
3715 author = RunGit(['config', 'user.email']).strip() or None
3716
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003717 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003718
3719 if args:
3720 if len(args) > 1:
3721 parser.error('Unknown args')
3722 base_branch = args[0]
3723 else:
3724 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003725 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003726
3727 change = cl.GetChange(base_branch, None)
3728 return owners_finder.OwnersFinder(
3729 [f.LocalPath() for f in
3730 cl.GetChange(base_branch, None).AffectedFiles()],
3731 change.RepositoryRoot(), author,
3732 fopen=file, os_path=os.path, glob=glob.glob,
3733 disable_color=options.no_color).run()
3734
3735
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003736def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003737 """Generates a diff command."""
3738 # Generate diff for the current branch's changes.
3739 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
3740 upstream_commit, '--' ]
3741
3742 if args:
3743 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003744 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003745 diff_cmd.append(arg)
3746 else:
3747 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003748
3749 return diff_cmd
3750
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003751def MatchingFileType(file_name, extensions):
3752 """Returns true if the file name ends with one of the given extensions."""
3753 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003754
enne@chromium.org555cfe42014-01-29 18:21:39 +00003755@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003756def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003757 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00003758 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00003759 GN_EXTS = ['.gn', '.gni']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003760 parser.add_option('--full', action='store_true',
3761 help='Reformat the full content of all touched files')
3762 parser.add_option('--dry-run', action='store_true',
3763 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003764 parser.add_option('--python', action='store_true',
3765 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003766 parser.add_option('--diff', action='store_true',
3767 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003768 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003769
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003770 # git diff generates paths against the root of the repository. Change
3771 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003772 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003773 if rel_base_path:
3774 os.chdir(rel_base_path)
3775
digit@chromium.org29e47272013-05-17 17:01:46 +00003776 # Grab the merge-base commit, i.e. the upstream commit of the current
3777 # branch when it was created or the last time it was rebased. This is
3778 # to cover the case where the user may have called "git fetch origin",
3779 # moving the origin branch to a newer commit, but hasn't rebased yet.
3780 upstream_commit = None
3781 cl = Changelist()
3782 upstream_branch = cl.GetUpstreamBranch()
3783 if upstream_branch:
3784 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
3785 upstream_commit = upstream_commit.strip()
3786
3787 if not upstream_commit:
3788 DieWithError('Could not find base commit for this branch. '
3789 'Are you in detached state?')
3790
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003791 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
3792 diff_output = RunGit(changed_files_cmd)
3793 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00003794 # Filter out files deleted by this CL
3795 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003796
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003797 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
3798 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
3799 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00003800 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00003801
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003802 top_dir = os.path.normpath(
3803 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
3804
3805 # Locate the clang-format binary in the checkout
3806 try:
3807 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
3808 except clang_format.NotFoundError, e:
3809 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00003810
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003811 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
3812 # formatted. This is used to block during the presubmit.
3813 return_value = 0
3814
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003815 if clang_diff_files:
3816 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003817 cmd = [clang_format_tool]
3818 if not opts.dry_run and not opts.diff:
3819 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003820 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003821 if opts.diff:
3822 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003823 else:
3824 env = os.environ.copy()
3825 env['PATH'] = str(os.path.dirname(clang_format_tool))
3826 try:
3827 script = clang_format.FindClangFormatScriptInChromiumTree(
3828 'clang-format-diff.py')
3829 except clang_format.NotFoundError, e:
3830 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003831
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003832 cmd = [sys.executable, script, '-p0']
3833 if not opts.dry_run and not opts.diff:
3834 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003835
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003836 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
3837 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003838
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003839 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
3840 if opts.diff:
3841 sys.stdout.write(stdout)
3842 if opts.dry_run and len(stdout) > 0:
3843 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003844
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003845 # Similar code to above, but using yapf on .py files rather than clang-format
3846 # on C/C++ files
3847 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003848 yapf_tool = gclient_utils.FindExecutable('yapf')
3849 if yapf_tool is None:
3850 DieWithError('yapf not found in PATH')
3851
3852 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003853 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003854 cmd = [yapf_tool]
3855 if not opts.dry_run and not opts.diff:
3856 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003857 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003858 if opts.diff:
3859 sys.stdout.write(stdout)
3860 else:
3861 # TODO(sbc): yapf --lines mode still has some issues.
3862 # https://github.com/google/yapf/issues/154
3863 DieWithError('--python currently only works with --full')
3864
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003865 # Dart's formatter does not have the nice property of only operating on
3866 # modified chunks, so hard code full.
3867 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003868 try:
3869 command = [dart_format.FindDartFmtToolInChromiumTree()]
3870 if not opts.dry_run and not opts.diff:
3871 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003872 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003873
ppi@chromium.org6593d932016-03-03 15:41:15 +00003874 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003875 if opts.dry_run and stdout:
3876 return_value = 2
3877 except dart_format.NotFoundError as e:
erikcorry@chromium.org3e445022015-12-17 09:07:26 +00003878 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
3879 'found in this checkout. Files in other languages are still ' +
3880 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003881
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00003882 # Format GN build files. Always run on full build files for canonical form.
3883 if gn_diff_files:
3884 cmd = ['gn', 'format']
3885 if not opts.dry_run and not opts.diff:
3886 cmd.append('--in-place')
3887 for gn_diff_file in gn_diff_files:
3888 stdout = RunCommand(cmd + [gn_diff_file], cwd=top_dir)
3889 if opts.diff:
3890 sys.stdout.write(stdout)
3891
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003892 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003893
3894
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00003895@subcommand.usage('<codereview url or issue id>')
3896def CMDcheckout(parser, args):
3897 """Checks out a branch associated with a given Rietveld issue."""
3898 _, args = parser.parse_args(args)
3899
3900 if len(args) != 1:
3901 parser.print_help()
3902 return 1
3903
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003904 target_issue = ParseIssueNum(args[0])
3905 if target_issue == None:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00003906 parser.print_help()
3907 return 1
3908
3909 key_and_issues = [x.split() for x in RunGit(
3910 ['config', '--local', '--get-regexp', r'branch\..*\.rietveldissue'])
3911 .splitlines()]
3912 branches = []
3913 for key, issue in key_and_issues:
3914 if issue == target_issue:
3915 branches.append(re.sub(r'branch\.(.*)\.rietveldissue', r'\1', key))
3916
3917 if len(branches) == 0:
3918 print 'No branch found for issue %s.' % target_issue
3919 return 1
3920 if len(branches) == 1:
3921 RunGit(['checkout', branches[0]])
3922 else:
3923 print 'Multiple branches match issue %s:' % target_issue
3924 for i in range(len(branches)):
3925 print '%d: %s' % (i, branches[i])
3926 which = raw_input('Choose by index: ')
3927 try:
3928 RunGit(['checkout', branches[int(which)]])
3929 except (IndexError, ValueError):
3930 print 'Invalid selection, not checking out any branch.'
3931 return 1
3932
3933 return 0
3934
3935
maruel@chromium.org29404b52014-09-08 22:58:00 +00003936def CMDlol(parser, args):
3937 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00003938 print zlib.decompress(base64.b64decode(
3939 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
3940 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
3941 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
3942 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00003943 return 0
3944
3945
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003946class OptionParser(optparse.OptionParser):
3947 """Creates the option parse and add --verbose support."""
3948 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003949 optparse.OptionParser.__init__(
3950 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003951 self.add_option(
3952 '-v', '--verbose', action='count', default=0,
3953 help='Use 2 times for more debugging info')
3954
3955 def parse_args(self, args=None, values=None):
3956 options, args = optparse.OptionParser.parse_args(self, args, values)
3957 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
3958 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
3959 return options, args
3960
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003961
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003962def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00003963 if sys.hexversion < 0x02060000:
3964 print >> sys.stderr, (
3965 '\nYour python version %s is unsupported, please upgrade.\n' %
3966 sys.version.split(' ', 1)[0])
3967 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003968
maruel@chromium.orgddd59412011-11-30 14:20:38 +00003969 # Reload settings.
3970 global settings
3971 settings = Settings()
3972
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003973 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003974 dispatcher = subcommand.CommandDispatcher(__name__)
3975 try:
3976 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003977 except auth.AuthenticationError as e:
3978 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003979 except urllib2.HTTPError, e:
3980 if e.code != 500:
3981 raise
3982 DieWithError(
3983 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
3984 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00003985 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003986
3987
3988if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003989 # These affect sys.stdout so do it outside of main() to simplify mocks in
3990 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00003991 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003992 colorama.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00003993 try:
3994 sys.exit(main(sys.argv[1:]))
3995 except KeyboardInterrupt:
3996 sys.stderr.write('interrupted\n')
3997 sys.exit(1)