blob: 1ded4aaf2a88296e702e9945cfff166328c704cb [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
1607
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001608@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001609def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001610 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001611
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001612 parser.add_option('--activate-update', action='store_true',
1613 help='activate auto-updating [rietveld] section in '
1614 '.git/config')
1615 parser.add_option('--deactivate-update', action='store_true',
1616 help='deactivate auto-updating [rietveld] section in '
1617 '.git/config')
1618 options, args = parser.parse_args(args)
1619
1620 if options.deactivate_update:
1621 RunGit(['config', 'rietveld.autoupdate', 'false'])
1622 return
1623
1624 if options.activate_update:
1625 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1626 return
1627
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001628 if len(args) == 0:
1629 GetCodereviewSettingsInteractively()
tandrii@chromium.org18630d62016-03-04 12:06:02 +00001630 DownloadGerritHook(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001631 return 0
1632
1633 url = args[0]
1634 if not url.endswith('codereview.settings'):
1635 url = os.path.join(url, 'codereview.settings')
1636
1637 # Load code review settings and download hooks (if available).
1638 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
tandrii@chromium.org18630d62016-03-04 12:06:02 +00001639 DownloadGerritHook(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001640 return 0
1641
1642
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001643def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001644 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001645 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1646 branch = ShortBranchName(branchref)
1647 _, args = parser.parse_args(args)
1648 if not args:
1649 print("Current base-url:")
1650 return RunGit(['config', 'branch.%s.base-url' % branch],
1651 error_ok=False).strip()
1652 else:
1653 print("Setting base-url to %s" % args[0])
1654 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1655 error_ok=False).strip()
1656
1657
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001658def color_for_status(status):
1659 """Maps a Changelist status to color, for CMDstatus and other tools."""
1660 return {
1661 'unsent': Fore.RED,
1662 'waiting': Fore.BLUE,
1663 'reply': Fore.YELLOW,
1664 'lgtm': Fore.GREEN,
1665 'commit': Fore.MAGENTA,
1666 'closed': Fore.CYAN,
1667 'error': Fore.WHITE,
1668 }.get(status, Fore.WHITE)
1669
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001670def fetch_cl_status(branch, auth_config=None):
1671 """Fetches information for an issue and returns (branch, issue, status)."""
1672 cl = Changelist(branchref=branch, auth_config=auth_config)
1673 url = cl.GetIssueURL()
1674 status = cl.GetStatus()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001675
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001676 if url and (not status or status == 'error'):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001677 # The issue probably doesn't exist anymore.
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001678 url += ' (broken)'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001679
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001680 return (branch, url, status)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001681
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001682def get_cl_statuses(
1683 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001684 """Returns a blocking iterable of (branch, issue, color) for given branches.
1685
1686 If fine_grained is true, this will fetch CL statuses from the server.
1687 Otherwise, simply indicate if there's a matching url for the given branches.
1688
1689 If max_processes is specified, it is used as the maximum number of processes
1690 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
1691 spawned.
1692 """
1693 # Silence upload.py otherwise it becomes unwieldly.
1694 upload.verbosity = 0
1695
1696 if fine_grained:
1697 # Process one branch synchronously to work through authentication, then
1698 # spawn processes to process all the other branches in parallel.
1699 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001700 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
1701 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001702
1703 branches_to_fetch = branches[1:]
1704 pool = ThreadPool(
1705 min(max_processes, len(branches_to_fetch))
1706 if max_processes is not None
1707 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001708 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001709 yield x
1710 else:
1711 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1712 for b in branches:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001713 cl = Changelist(branchref=b, auth_config=auth_config)
1714 url = cl.GetIssueURL()
1715 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001716
rmistry@google.com2dd99862015-06-22 12:22:18 +00001717
1718def upload_branch_deps(cl, args):
1719 """Uploads CLs of local branches that are dependents of the current branch.
1720
1721 If the local branch dependency tree looks like:
1722 test1 -> test2.1 -> test3.1
1723 -> test3.2
1724 -> test2.2 -> test3.3
1725
1726 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
1727 run on the dependent branches in this order:
1728 test2.1, test3.1, test3.2, test2.2, test3.3
1729
1730 Note: This function does not rebase your local dependent branches. Use it when
1731 you make a change to the parent branch that will not conflict with its
1732 dependent branches, and you would like their dependencies updated in
1733 Rietveld.
1734 """
1735 if git_common.is_dirty_git_tree('upload-branch-deps'):
1736 return 1
1737
1738 root_branch = cl.GetBranch()
1739 if root_branch is None:
1740 DieWithError('Can\'t find dependent branches from detached HEAD state. '
1741 'Get on a branch!')
1742 if not cl.GetIssue() or not cl.GetPatchset():
1743 DieWithError('Current branch does not have an uploaded CL. We cannot set '
1744 'patchset dependencies without an uploaded CL.')
1745
1746 branches = RunGit(['for-each-ref',
1747 '--format=%(refname:short) %(upstream:short)',
1748 'refs/heads'])
1749 if not branches:
1750 print('No local branches found.')
1751 return 0
1752
1753 # Create a dictionary of all local branches to the branches that are dependent
1754 # on it.
1755 tracked_to_dependents = collections.defaultdict(list)
1756 for b in branches.splitlines():
1757 tokens = b.split()
1758 if len(tokens) == 2:
1759 branch_name, tracked = tokens
1760 tracked_to_dependents[tracked].append(branch_name)
1761
1762 print
1763 print 'The dependent local branches of %s are:' % root_branch
1764 dependents = []
1765 def traverse_dependents_preorder(branch, padding=''):
1766 dependents_to_process = tracked_to_dependents.get(branch, [])
1767 padding += ' '
1768 for dependent in dependents_to_process:
1769 print '%s%s' % (padding, dependent)
1770 dependents.append(dependent)
1771 traverse_dependents_preorder(dependent, padding)
1772 traverse_dependents_preorder(root_branch)
1773 print
1774
1775 if not dependents:
1776 print 'There are no dependent local branches for %s' % root_branch
1777 return 0
1778
1779 print ('This command will checkout all dependent branches and run '
1780 '"git cl upload".')
1781 ask_for_data('[Press enter to continue or ctrl-C to quit]')
1782
andybons@chromium.org962f9462016-02-03 20:00:42 +00001783 # Add a default patchset title to all upload calls in Rietveld.
1784 if not settings.GetIsGerrit():
1785 args.extend(['-t', 'Updated patchset dependency'])
1786
rmistry@google.com2dd99862015-06-22 12:22:18 +00001787 # Record all dependents that failed to upload.
1788 failures = {}
1789 # Go through all dependents, checkout the branch and upload.
1790 try:
1791 for dependent_branch in dependents:
1792 print
1793 print '--------------------------------------'
1794 print 'Running "git cl upload" from %s:' % dependent_branch
1795 RunGit(['checkout', '-q', dependent_branch])
1796 print
1797 try:
1798 if CMDupload(OptionParser(), args) != 0:
1799 print 'Upload failed for %s!' % dependent_branch
1800 failures[dependent_branch] = 1
1801 except: # pylint: disable=W0702
1802 failures[dependent_branch] = 1
1803 print
1804 finally:
1805 # Swap back to the original root branch.
1806 RunGit(['checkout', '-q', root_branch])
1807
1808 print
1809 print 'Upload complete for dependent branches!'
1810 for dependent_branch in dependents:
1811 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
1812 print ' %s : %s' % (dependent_branch, upload_status)
1813 print
1814
1815 return 0
1816
1817
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001818def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001819 """Show status of changelists.
1820
1821 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001822 - Red not sent for review or broken
1823 - Blue waiting for review
1824 - Yellow waiting for you to reply to review
1825 - Green LGTM'ed
1826 - Magenta in the commit queue
1827 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001828
1829 Also see 'git cl comments'.
1830 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001831 parser.add_option('--field',
1832 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001833 parser.add_option('-f', '--fast', action='store_true',
1834 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001835 parser.add_option(
1836 '-j', '--maxjobs', action='store', type=int,
1837 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001838
1839 auth.add_auth_options(parser)
1840 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001841 if args:
1842 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001843 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001844
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001845 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001846 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001847 if options.field.startswith('desc'):
1848 print cl.GetDescription()
1849 elif options.field == 'id':
1850 issueid = cl.GetIssue()
1851 if issueid:
1852 print issueid
1853 elif options.field == 'patch':
1854 patchset = cl.GetPatchset()
1855 if patchset:
1856 print patchset
1857 elif options.field == 'url':
1858 url = cl.GetIssueURL()
1859 if url:
1860 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001861 return 0
1862
1863 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1864 if not branches:
1865 print('No local branch found.')
1866 return 0
1867
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001868 changes = (
1869 Changelist(branchref=b, auth_config=auth_config)
1870 for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001871 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001872 alignment = max(5, max(len(b) for b in branches))
1873 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001874 output = get_cl_statuses(branches,
1875 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001876 max_processes=options.maxjobs,
1877 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001878
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001879 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001880 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001881 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001882 while branch not in branch_statuses:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001883 b, i, status = output.next()
1884 branch_statuses[b] = (i, status)
1885 issue_url, status = branch_statuses.pop(branch)
1886 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001887 reset = Fore.RESET
1888 if not sys.stdout.isatty():
1889 color = ''
1890 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001891 status_str = '(%s)' % status if status else ''
1892 print ' %*s : %s%s %s%s' % (
1893 alignment, ShortBranchName(branch), color, issue_url, status_str,
1894 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001895
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001896 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001897 print
1898 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001899 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00001900 if not cl.GetIssue():
1901 print 'No issue assigned.'
1902 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001903 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00001904 if not options.fast:
1905 print 'Issue description:'
1906 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001907 return 0
1908
1909
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001910def colorize_CMDstatus_doc():
1911 """To be called once in main() to add colors to git cl status help."""
1912 colors = [i for i in dir(Fore) if i[0].isupper()]
1913
1914 def colorize_line(line):
1915 for color in colors:
1916 if color in line.upper():
1917 # Extract whitespaces first and the leading '-'.
1918 indent = len(line) - len(line.lstrip(' ')) + 1
1919 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1920 return line
1921
1922 lines = CMDstatus.__doc__.splitlines()
1923 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1924
1925
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001926@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001927def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001928 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001929
1930 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001931 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00001932 parser.add_option('-r', '--reverse', action='store_true',
1933 help='Lookup the branch(es) for the specified issues. If '
1934 'no issues are specified, all branches with mapped '
1935 'issues will be listed.')
1936 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001937
dnj@chromium.org406c4402015-03-03 17:22:28 +00001938 if options.reverse:
1939 branches = RunGit(['for-each-ref', 'refs/heads',
1940 '--format=%(refname:short)']).splitlines()
1941
1942 # Reverse issue lookup.
1943 issue_branch_map = {}
1944 for branch in branches:
1945 cl = Changelist(branchref=branch)
1946 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
1947 if not args:
1948 args = sorted(issue_branch_map.iterkeys())
1949 for issue in args:
1950 if not issue:
1951 continue
1952 print 'Branch for issue number %s: %s' % (
1953 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
1954 else:
1955 cl = Changelist()
1956 if len(args) > 0:
1957 try:
1958 issue = int(args[0])
1959 except ValueError:
1960 DieWithError('Pass a number to set the issue or none to list it.\n'
1961 'Maybe you want to run git cl status?')
1962 cl.SetIssue(issue)
1963 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001964 return 0
1965
1966
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001967def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001968 """Shows or posts review comments for any changelist."""
1969 parser.add_option('-a', '--add-comment', dest='comment',
1970 help='comment to add to an issue')
1971 parser.add_option('-i', dest='issue',
1972 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00001973 parser.add_option('-j', '--json-file',
1974 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001975 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001976 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001977 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001978
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001979 issue = None
1980 if options.issue:
1981 try:
1982 issue = int(options.issue)
1983 except ValueError:
1984 DieWithError('A review issue id is expected to be a number')
1985
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001986 cl = Changelist(issue=issue, auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001987
1988 if options.comment:
1989 cl.AddComment(options.comment)
1990 return 0
1991
1992 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00001993 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00001994 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00001995 summary.append({
1996 'date': message['date'],
1997 'lgtm': False,
1998 'message': message['text'],
1999 'not_lgtm': False,
2000 'sender': message['sender'],
2001 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002002 if message['disapproval']:
2003 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00002004 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002005 elif message['approval']:
2006 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00002007 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002008 elif message['sender'] == data['owner_email']:
2009 color = Fore.MAGENTA
2010 else:
2011 color = Fore.BLUE
2012 print '\n%s%s %s%s' % (
2013 color, message['date'].split('.', 1)[0], message['sender'],
2014 Fore.RESET)
2015 if message['text'].strip():
2016 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00002017 if options.json_file:
2018 with open(options.json_file, 'wb') as f:
2019 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002020 return 0
2021
2022
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002023def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002024 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00002025 parser.add_option('-d', '--display', action='store_true',
2026 help='Display the description instead of opening an editor')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002027 auth.add_auth_options(parser)
2028 options, _ = parser.parse_args(args)
2029 auth_config = auth.extract_auth_config_from_options(options)
2030 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002031 if not cl.GetIssue():
2032 DieWithError('This branch has no associated changelist.')
2033 description = ChangeDescription(cl.GetDescription())
smut@google.com34fb6b12015-07-13 20:03:26 +00002034 if options.display:
2035 print description.description
2036 return 0
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002037 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00002038 if cl.GetDescription() != description.description:
2039 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002040 return 0
2041
2042
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002043def CreateDescriptionFromLog(args):
2044 """Pulls out the commit log to use as a base for the CL description."""
2045 log_args = []
2046 if len(args) == 1 and not args[0].endswith('.'):
2047 log_args = [args[0] + '..']
2048 elif len(args) == 1 and args[0].endswith('...'):
2049 log_args = [args[0][:-1]]
2050 elif len(args) == 2:
2051 log_args = [args[0] + '..' + args[1]]
2052 else:
2053 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00002054 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002055
2056
thestig@chromium.org44202a22014-03-11 19:22:18 +00002057def CMDlint(parser, args):
2058 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002059 parser.add_option('--filter', action='append', metavar='-x,+y',
2060 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002061 auth.add_auth_options(parser)
2062 options, args = parser.parse_args(args)
2063 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002064
2065 # Access to a protected member _XX of a client class
2066 # pylint: disable=W0212
2067 try:
2068 import cpplint
2069 import cpplint_chromium
2070 except ImportError:
2071 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
2072 return 1
2073
2074 # Change the current working directory before calling lint so that it
2075 # shows the correct base.
2076 previous_cwd = os.getcwd()
2077 os.chdir(settings.GetRoot())
2078 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002079 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002080 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
2081 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00002082 if not files:
2083 print "Cannot lint an empty CL"
2084 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00002085
2086 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002087 command = args + files
2088 if options.filter:
2089 command = ['--filter=' + ','.join(options.filter)] + command
2090 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002091
2092 white_regex = re.compile(settings.GetLintRegex())
2093 black_regex = re.compile(settings.GetLintIgnoreRegex())
2094 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
2095 for filename in filenames:
2096 if white_regex.match(filename):
2097 if black_regex.match(filename):
2098 print "Ignoring file %s" % filename
2099 else:
2100 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
2101 extra_check_functions)
2102 else:
2103 print "Skipping file %s" % filename
2104 finally:
2105 os.chdir(previous_cwd)
2106 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
2107 if cpplint._cpplint_state.error_count != 0:
2108 return 1
2109 return 0
2110
2111
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002112def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002113 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002114 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002115 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002116 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00002117 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002118 auth.add_auth_options(parser)
2119 options, args = parser.parse_args(args)
2120 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002121
sbc@chromium.org71437c02015-04-09 19:29:40 +00002122 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00002123 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002124 return 1
2125
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002126 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002127 if args:
2128 base_branch = args[0]
2129 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002130 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002131 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002132
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002133 cl.RunHook(
2134 committing=not options.upload,
2135 may_prompt=False,
2136 verbose=options.verbose,
2137 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00002138 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002139
2140
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002141def AddChangeIdToCommitMessage(options, args):
2142 """Re-commits using the current message, assumes the commit hook is in
2143 place.
2144 """
2145 log_desc = options.message or CreateDescriptionFromLog(args)
2146 git_command = ['commit', '--amend', '-m', log_desc]
2147 RunGit(git_command)
2148 new_log_desc = CreateDescriptionFromLog(args)
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00002149 if git_footers.get_footer_change_id(new_log_desc):
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002150 print 'git-cl: Added Change-Id to commit message.'
2151 else:
2152 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
2153
2154
tandrii@chromium.org65874e12016-03-04 12:03:02 +00002155def GenerateGerritChangeId(message):
2156 """Returns Ixxxxxx...xxx change id.
2157
2158 Works the same way as
2159 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
2160 but can be called on demand on all platforms.
2161
2162 The basic idea is to generate git hash of a state of the tree, original commit
2163 message, author/committer info and timestamps.
2164 """
2165 lines = []
2166 tree_hash = RunGitSilent(['write-tree'])
2167 lines.append('tree %s' % tree_hash.strip())
2168 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
2169 if code == 0:
2170 lines.append('parent %s' % parent.strip())
2171 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
2172 lines.append('author %s' % author.strip())
2173 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
2174 lines.append('committer %s' % committer.strip())
2175 lines.append('')
2176 # Note: Gerrit's commit-hook actually cleans message of some lines and
2177 # whitespace. This code is not doing this, but it clearly won't decrease
2178 # entropy.
2179 lines.append(message)
2180 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
2181 stdin='\n'.join(lines))
2182 return 'I%s' % change_hash.strip()
2183
2184
piman@chromium.org336f9122014-09-04 02:16:55 +00002185def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002186 """upload the current branch to gerrit."""
2187 # We assume the remote called "origin" is the one we want.
2188 # It is probably not worthwhile to support different workflows.
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002189 gerrit_remote = 'origin'
luqui@chromium.org609f3952015-05-04 22:47:04 +00002190
2191 remote, remote_branch = cl.GetRemoteBranch()
2192 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2193 pending_prefix='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002194
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002195 change_desc = ChangeDescription(
2196 options.message or CreateDescriptionFromLog(args))
2197 if not change_desc.description:
andybons@chromium.org962f9462016-02-03 20:00:42 +00002198 print "\nDescription is empty. Aborting..."
2199 return 1
2200
2201 if options.title:
2202 print "\nPatch titles (-t) are not supported in Gerrit. Aborting..."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002203 return 1
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002204
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002205 if options.squash:
2206 # Try to get the message from a previous upload.
2207 shadow_branch = 'refs/heads/git_cl_uploads/' + cl.GetBranch()
bauerb@chromium.org13502e02016-02-18 10:18:29 +00002208 message = RunGitSilent(['show', '--format=%B', '-s', shadow_branch])
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002209 if not message:
2210 if not options.force:
2211 change_desc.prompt()
2212
2213 if CHANGE_ID not in change_desc.description:
2214 # Run the commit-msg hook without modifying the head commit by writing
2215 # the commit message to a temporary file and running the hook over it,
2216 # then reading the file back in.
2217 commit_msg_hook = os.path.join(settings.GetRoot(), '.git', 'hooks',
2218 'commit-msg')
2219 file_handle, msg_file = tempfile.mkstemp(text=True,
2220 prefix='commit_msg')
tandrii@chromium.orga83663a2016-01-14 16:01:00 +00002221 logging.debug("%s %s", file_handle, msg_file)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002222 try:
2223 try:
tandrii@chromium.orga83663a2016-01-14 16:01:00 +00002224 try:
2225 fileobj = os.fdopen(file_handle, 'w')
2226 except OSError:
2227 # if fdopen fails, file_handle remains open.
2228 # See https://docs.python.org/2/library/os.html#os.fdopen.
2229 os.close(file_handle)
2230 raise
2231 with fileobj:
2232 # This will close the file_handle.
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002233 fileobj.write(change_desc.description)
tandrii@chromium.orga83663a2016-01-14 16:01:00 +00002234 logging.debug("%s %s finish editing", file_handle, msg_file)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002235 finally:
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002236 RunCommand([commit_msg_hook, msg_file])
2237 change_desc.set_description(gclient_utils.FileRead(msg_file))
2238 finally:
2239 os.remove(msg_file)
2240
2241 if not change_desc.description:
2242 print "Description is empty; aborting."
2243 return 1
2244
2245 message = change_desc.description
2246
2247 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2248 if remote is '.':
2249 # If our upstream branch is local, we base our squashed commit on its
2250 # squashed version.
2251 parent = ('refs/heads/git_cl_uploads/' +
2252 scm.GIT.ShortBranchName(upstream_branch))
2253
2254 # Verify that the upstream branch has been uploaded too, otherwise Gerrit
2255 # will create additional CLs when uploading.
2256 if (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2257 RunGitSilent(['rev-parse', parent + ':'])):
2258 print 'Upload upstream branch ' + upstream_branch + ' first.'
2259 return 1
2260 else:
2261 parent = cl.GetCommonAncestorWithUpstream()
2262
2263 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2264 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2265 '-m', message]).strip()
2266 else:
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00002267 if not git_footers.get_footer_change_id(change_desc.description):
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002268 AddChangeIdToCommitMessage(options, args)
2269 ref_to_push = 'HEAD'
2270 parent = '%s/%s' % (gerrit_remote, branch)
2271
2272 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2273 ref_to_push)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002274 if len(commits) > 1:
2275 print('WARNING: This will upload %d commits. Run the following command '
2276 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002277 print('git log %s..%s' % (parent, ref_to_push))
2278 print('You can also use `git squash-branch` to squash these into a single '
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002279 'commit.')
2280 ask_for_data('About to upload; enter to confirm.')
2281
piman@chromium.org336f9122014-09-04 02:16:55 +00002282 if options.reviewers or options.tbr_owners:
2283 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002284
ukai@chromium.orge8077812012-02-03 03:41:46 +00002285 receive_options = []
2286 cc = cl.GetCCList().split(',')
2287 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002288 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002289 cc = filter(None, cc)
2290 if cc:
2291 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002292 if change_desc.get_reviewers():
2293 receive_options.extend(
2294 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002295
ukai@chromium.orge8077812012-02-03 03:41:46 +00002296 git_command = ['push']
2297 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00002298 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00002299 ' '.join(receive_options))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002300 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
ukai@chromium.orge8077812012-02-03 03:41:46 +00002301 RunGit(git_command)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002302
2303 if options.squash:
2304 head = RunGit(['rev-parse', 'HEAD']).strip()
2305 RunGit(['update-ref', '-m', 'Uploaded ' + head, shadow_branch, ref_to_push])
2306
ukai@chromium.orge8077812012-02-03 03:41:46 +00002307 # TODO(ukai): parse Change-Id: and set issue number?
2308 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002309
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002310
wittman@chromium.org455dc922015-01-26 20:15:50 +00002311def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
2312 """Computes the remote branch ref to use for the CL.
2313
2314 Args:
2315 remote (str): The git remote for the CL.
2316 remote_branch (str): The git remote branch for the CL.
2317 target_branch (str): The target branch specified by the user.
2318 pending_prefix (str): The pending prefix from the settings.
2319 """
2320 if not (remote and remote_branch):
2321 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002322
wittman@chromium.org455dc922015-01-26 20:15:50 +00002323 if target_branch:
2324 # Cannonicalize branch references to the equivalent local full symbolic
2325 # refs, which are then translated into the remote full symbolic refs
2326 # below.
2327 if '/' not in target_branch:
2328 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
2329 else:
2330 prefix_replacements = (
2331 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
2332 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
2333 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
2334 )
2335 match = None
2336 for regex, replacement in prefix_replacements:
2337 match = re.search(regex, target_branch)
2338 if match:
2339 remote_branch = target_branch.replace(match.group(0), replacement)
2340 break
2341 if not match:
2342 # This is a branch path but not one we recognize; use as-is.
2343 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00002344 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
2345 # Handle the refs that need to land in different refs.
2346 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002347
wittman@chromium.org455dc922015-01-26 20:15:50 +00002348 # Create the true path to the remote branch.
2349 # Does the following translation:
2350 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
2351 # * refs/remotes/origin/master -> refs/heads/master
2352 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
2353 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
2354 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
2355 elif remote_branch.startswith('refs/remotes/%s/' % remote):
2356 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
2357 'refs/heads/')
2358 elif remote_branch.startswith('refs/remotes/branch-heads'):
2359 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
2360 # If a pending prefix exists then replace refs/ with it.
2361 if pending_prefix:
2362 remote_branch = remote_branch.replace('refs/', pending_prefix)
2363 return remote_branch
2364
2365
piman@chromium.org336f9122014-09-04 02:16:55 +00002366def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002367 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002368 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2369 upload_args.extend(['--server', cl.GetRietveldServer()])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002370 upload_args.extend(auth.auth_config_to_command_options(cl.auth_config))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002371 if options.emulate_svn_auto_props:
2372 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002373
2374 change_desc = None
2375
pgervais@chromium.org91141372014-01-09 23:27:20 +00002376 if options.email is not None:
2377 upload_args.extend(['--email', options.email])
2378
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002379 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002380 if options.title:
2381 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00002382 if options.message:
2383 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00002384 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002385 print ("This branch is associated with issue %s. "
2386 "Adding patch to that issue." % cl.GetIssue())
2387 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002388 if options.title:
2389 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00002390 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002391 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00002392 if options.reviewers or options.tbr_owners:
2393 change_desc.update_reviewers(options.reviewers,
2394 options.tbr_owners,
2395 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002396 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002397 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002398
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002399 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002400 print "Description is empty; aborting."
2401 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002402
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002403 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002404 if change_desc.get_reviewers():
2405 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00002406 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002407 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00002408 DieWithError("Must specify reviewers to send email.")
2409 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00002410
2411 # We check this before applying rietveld.private assuming that in
2412 # rietveld.cc only addresses which we can send private CLs to are listed
2413 # if rietveld.private is set, and so we should ignore rietveld.cc only when
2414 # --private is specified explicitly on the command line.
2415 if options.private:
2416 logging.warn('rietveld.cc is ignored since private flag is specified. '
2417 'You need to review and add them manually if necessary.')
2418 cc = cl.GetCCListWithoutDefault()
2419 else:
2420 cc = cl.GetCCList()
2421 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00002422 if cc:
2423 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002424
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002425 if options.private or settings.GetDefaultPrivateFlag() == "True":
2426 upload_args.append('--private')
2427
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002428 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00002429 if not options.find_copies:
2430 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002431
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002432 # Include the upstream repo's URL in the change -- this is useful for
2433 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002434 remote_url = cl.GetGitBaseUrlFromConfig()
2435 if not remote_url:
2436 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002437 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002438 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00002439 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
2440 remote_url = (cl.GetRemoteUrl() + '@'
2441 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002442 if remote_url:
2443 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00002444 remote, remote_branch = cl.GetRemoteBranch()
wittman@chromium.org455dc922015-01-26 20:15:50 +00002445 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2446 settings.GetPendingRefPrefix())
2447 if target_ref:
2448 upload_args.extend(['--target_ref', target_ref])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002449
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002450 # Look for dependent patchsets. See crbug.com/480453 for more details.
2451 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2452 upstream_branch = ShortBranchName(upstream_branch)
2453 if remote is '.':
2454 # A local branch is being tracked.
2455 local_branch = ShortBranchName(upstream_branch)
rmistry@google.com78948ed2015-07-08 23:09:57 +00002456 if settings.GetIsSkipDependencyUpload(local_branch):
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002457 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002458 print ('Skipping dependency patchset upload because git config '
2459 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002460 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002461 else:
2462 auth_config = auth.extract_auth_config_from_options(options)
2463 branch_cl = Changelist(branchref=local_branch, auth_config=auth_config)
2464 branch_cl_issue_url = branch_cl.GetIssueURL()
2465 branch_cl_issue = branch_cl.GetIssue()
2466 branch_cl_patchset = branch_cl.GetPatchset()
2467 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2468 upload_args.extend(
2469 ['--depends_on_patchset', '%s:%s' % (
2470 branch_cl_issue, branch_cl_patchset)])
2471 print
2472 print ('The current branch (%s) is tracking a local branch (%s) with '
2473 'an associated CL.') % (cl.GetBranch(), local_branch)
2474 print 'Adding %s/#ps%s as a dependency patchset.' % (
2475 branch_cl_issue_url, branch_cl_patchset)
2476 print
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002477
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002478 project = settings.GetProject()
2479 if project:
2480 upload_args.extend(['--project', project])
2481
rmistry@google.comef966222015-04-07 11:15:01 +00002482 if options.cq_dry_run:
2483 upload_args.extend(['--cq_dry_run'])
2484
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002485 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00002486 upload_args = ['upload'] + upload_args + args
2487 logging.info('upload.RealMain(%s)', upload_args)
2488 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00002489 issue = int(issue)
2490 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00002491 except KeyboardInterrupt:
2492 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002493 except:
2494 # If we got an exception after the user typed a description for their
2495 # change, back up the description before re-raising.
2496 if change_desc:
2497 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2498 print '\nGot exception while uploading -- saving description to %s\n' \
2499 % backup_path
2500 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002501 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002502 backup_file.close()
2503 raise
2504
2505 if not cl.GetIssue():
2506 cl.SetIssue(issue)
2507 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002508
2509 if options.use_commit_queue:
2510 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002511 return 0
2512
2513
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002514def cleanup_list(l):
2515 """Fixes a list so that comma separated items are put as individual items.
2516
2517 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
2518 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
2519 """
2520 items = sum((i.split(',') for i in l), [])
2521 stripped_items = (i.strip() for i in items)
2522 return sorted(filter(None, stripped_items))
2523
2524
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002525@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002526def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00002527 """Uploads the current changelist to codereview.
2528
2529 Can skip dependency patchset uploads for a branch by running:
2530 git config branch.branch_name.skip-deps-uploads True
2531 To unset run:
2532 git config --unset branch.branch_name.skip-deps-uploads
2533 Can also set the above globally by using the --global flag.
2534 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00002535 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2536 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002537 parser.add_option('--bypass-watchlists', action='store_true',
2538 dest='bypass_watchlists',
2539 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002540 parser.add_option('-f', action='store_true', dest='force',
2541 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002542 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00002543 parser.add_option('-t', dest='title',
2544 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002545 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002546 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002547 help='reviewer email addresses')
2548 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002549 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002550 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00002551 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00002552 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002553 parser.add_option('--emulate_svn_auto_props',
2554 '--emulate-svn-auto-props',
2555 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00002556 dest="emulate_svn_auto_props",
2557 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00002558 parser.add_option('-c', '--use-commit-queue', action='store_true',
2559 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002560 parser.add_option('--private', action='store_true',
2561 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00002562 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002563 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00002564 metavar='TARGET',
2565 help='Apply CL to remote ref TARGET. ' +
2566 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002567 parser.add_option('--squash', action='store_true',
2568 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002569 parser.add_option('--no-squash', action='store_true',
2570 help='Don\'t squash multiple commits into one ' +
2571 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002572 parser.add_option('--email', default=None,
2573 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00002574 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
2575 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00002576 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
2577 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00002578 help='Send the patchset to do a CQ dry run right after '
2579 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00002580 parser.add_option('--dependencies', action='store_true',
2581 help='Uploads CLs of all the local branches that depend on '
2582 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002583
rmistry@google.com2dd99862015-06-22 12:22:18 +00002584 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002585 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002586 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002587 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002588 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002589
sbc@chromium.org71437c02015-04-09 19:29:40 +00002590 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002591 return 1
2592
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002593 options.reviewers = cleanup_list(options.reviewers)
2594 options.cc = cleanup_list(options.cc)
2595
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002596 cl = Changelist(auth_config=auth_config)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002597 if args:
2598 # TODO(ukai): is it ok for gerrit case?
2599 base_branch = args[0]
2600 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00002601 if cl.GetBranch() is None:
2602 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
2603
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002604 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002605 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00002606 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00002607
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00002608 # Make sure authenticated to Rietveld before running expensive hooks. It is
2609 # a fast, best efforts check. Rietveld still can reject the authentication
2610 # during the actual upload.
2611 if not settings.GetIsGerrit() and auth_config.use_oauth2:
2612 authenticator = auth.get_authenticator_for_host(
2613 cl.GetRietveldServer(), auth_config)
2614 if not authenticator.has_cached_credentials():
2615 raise auth.LoginRequiredError(cl.GetRietveldServer())
2616
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002617 # Apply watchlists on upload.
2618 change = cl.GetChange(base_branch, None)
2619 watchlist = watchlists.Watchlists(change.RepositoryRoot())
2620 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002621 if not options.bypass_watchlists:
2622 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002623
ukai@chromium.orge8077812012-02-03 03:41:46 +00002624 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00002625 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002626 # Set the reviewer list now so that presubmit checks can access it.
2627 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00002628 change_description.update_reviewers(options.reviewers,
2629 options.tbr_owners,
2630 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002631 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002632 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00002633 may_prompt=not options.force,
2634 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002635 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002636 if not hook_results.should_continue():
2637 return 1
2638 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002639 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002640
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002641 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002642 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002643 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00002644 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002645 print ('The last upload made from this repository was patchset #%d but '
2646 'the most recent patchset on the server is #%d.'
2647 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00002648 print ('Uploading will still work, but if you\'ve uploaded to this issue '
2649 'from another machine or branch the patch you\'re uploading now '
2650 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002651 ask_for_data('About to upload; enter to confirm.')
2652
iannucci@chromium.org79540052012-10-19 23:15:26 +00002653 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002654 if settings.GetIsGerrit():
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002655 if options.squash and options.no_squash:
2656 DieWithError('Can only use one of --squash or --no-squash')
2657
2658 options.squash = ((settings.GetSquashGerritUploads() or options.squash) and
2659 not options.no_squash)
2660
tandrii@chromium.org1e67bb72016-02-11 12:15:49 +00002661 ret = GerritUpload(options, args, cl, change)
2662 else:
2663 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002664 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00002665 git_set_branch_value('last-upload-hash',
2666 RunGit(['rev-parse', 'HEAD']).strip())
rmistry@google.com5626a922015-02-26 14:03:30 +00002667 # Run post upload hooks, if specified.
2668 if settings.GetRunPostUploadHook():
2669 presubmit_support.DoPostUploadExecuter(
2670 change,
2671 cl,
2672 settings.GetRoot(),
2673 options.verbose,
2674 sys.stdout)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002675
rmistry@google.com2dd99862015-06-22 12:22:18 +00002676 # Upload all dependencies if specified.
2677 if options.dependencies:
2678 print
2679 print '--dependencies has been specified.'
2680 print 'All dependent local branches will be re-uploaded.'
2681 print
2682 # Remove the dependencies flag from args so that we do not end up in a
2683 # loop.
2684 orig_args.remove('--dependencies')
2685 upload_branch_deps(cl, orig_args)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002686 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00002687
2688
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002689def IsSubmoduleMergeCommit(ref):
2690 # When submodules are added to the repo, we expect there to be a single
2691 # non-git-svn merge commit at remote HEAD with a signature comment.
2692 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00002693 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002694 return RunGit(cmd) != ''
2695
2696
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002697def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002698 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002699
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002700 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002701 Updates changelog with metadata (e.g. pointer to review).
2702 Pushes/dcommits the code upstream.
2703 Updates review and closes.
2704 """
2705 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2706 help='bypass upload presubmit hook')
2707 parser.add_option('-m', dest='message',
2708 help="override review description")
2709 parser.add_option('-f', action='store_true', dest='force',
2710 help="force yes to questions (don't prompt)")
2711 parser.add_option('-c', dest='contributor',
2712 help="external contributor for patch (appended to " +
2713 "description and used as author for git). Should be " +
2714 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002715 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002716 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002717 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002718 auth_config = auth.extract_auth_config_from_options(options)
2719
2720 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002721
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002722 current = cl.GetBranch()
2723 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2724 if not settings.GetIsGitSvn() and remote == '.':
2725 print
2726 print 'Attempting to push branch %r into another local branch!' % current
2727 print
2728 print 'Either reparent this branch on top of origin/master:'
2729 print ' git reparent-branch --root'
2730 print
2731 print 'OR run `git rebase-update` if you think the parent branch is already'
2732 print 'committed.'
2733 print
2734 print ' Current parent: %r' % upstream_branch
2735 return 1
2736
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002737 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002738 # Default to merging against our best guess of the upstream branch.
2739 args = [cl.GetUpstreamBranch()]
2740
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002741 if options.contributor:
2742 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
2743 print "Please provide contibutor as 'First Last <email@example.com>'"
2744 return 1
2745
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002746 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002747 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002748
sbc@chromium.org71437c02015-04-09 19:29:40 +00002749 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002750 return 1
2751
2752 # This rev-list syntax means "show all commits not in my branch that
2753 # are in base_branch".
2754 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
2755 base_branch]).splitlines()
2756 if upstream_commits:
2757 print ('Base branch "%s" has %d commits '
2758 'not in this branch.' % (base_branch, len(upstream_commits)))
2759 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
2760 return 1
2761
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002762 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002763 svn_head = None
2764 if cmd == 'dcommit' or base_has_submodules:
2765 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
2766 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002767
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002768 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002769 # If the base_head is a submodule merge commit, the first parent of the
2770 # base_head should be a git-svn commit, which is what we're interested in.
2771 base_svn_head = base_branch
2772 if base_has_submodules:
2773 base_svn_head += '^1'
2774
2775 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002776 if extra_commits:
2777 print ('This branch has %d additional commits not upstreamed yet.'
2778 % len(extra_commits.splitlines()))
2779 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
2780 'before attempting to %s.' % (base_branch, cmd))
2781 return 1
2782
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002783 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002784 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002785 author = None
2786 if options.contributor:
2787 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002788 hook_results = cl.RunHook(
2789 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002790 may_prompt=not options.force,
2791 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002792 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002793 if not hook_results.should_continue():
2794 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002795
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002796 # Check the tree status if the tree status URL is set.
2797 status = GetTreeStatus()
2798 if 'closed' == status:
2799 print('The tree is closed. Please wait for it to reopen. Use '
2800 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2801 return 1
2802 elif 'unknown' == status:
2803 print('Unable to determine tree status. Please verify manually and '
2804 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2805 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002806
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002807 change_desc = ChangeDescription(options.message)
2808 if not change_desc.description and cl.GetIssue():
2809 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002810
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002811 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00002812 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002813 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00002814 else:
2815 print 'No description set.'
2816 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
2817 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002818
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002819 # Keep a separate copy for the commit message, because the commit message
2820 # contains the link to the Rietveld issue, while the Rietveld message contains
2821 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002822 # Keep a separate copy for the commit message.
2823 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00002824 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002825
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002826 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00002827 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00002828 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00002829 # after it. Add a period on a new line to circumvent this. Also add a space
2830 # before the period to make sure that Gitiles continues to correctly resolve
2831 # the URL.
2832 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002833 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002834 commit_desc.append_footer('Patch from %s.' % options.contributor)
2835
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00002836 print('Description:')
2837 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002838
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002839 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002840 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00002841 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002842
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002843 # We want to squash all this branch's commits into one commit with the proper
2844 # description. We do this by doing a "reset --soft" to the base branch (which
2845 # keeps the working copy the same), then dcommitting that. If origin/master
2846 # has a submodule merge commit, we'll also need to cherry-pick the squashed
2847 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002848 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002849 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
2850 # Delete the branches if they exist.
2851 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
2852 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
2853 result = RunGitWithCode(showref_cmd)
2854 if result[0] == 0:
2855 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002856
2857 # We might be in a directory that's present in this branch but not in the
2858 # trunk. Move up to the top of the tree so that git commands that expect a
2859 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002860 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002861 if rel_base_path:
2862 os.chdir(rel_base_path)
2863
2864 # Stuff our change into the merge branch.
2865 # We wrap in a try...finally block so if anything goes wrong,
2866 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002867 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002868 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002869 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002870 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002871 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00002872 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002873 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002874 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002875 RunGit(
2876 [
2877 'commit', '--author', options.contributor,
2878 '-m', commit_desc.description,
2879 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002880 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002881 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002882 if base_has_submodules:
2883 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
2884 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
2885 RunGit(['checkout', CHERRY_PICK_BRANCH])
2886 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002887 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002888 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002889 pending_prefix = settings.GetPendingRefPrefix()
2890 if not pending_prefix or branch.startswith(pending_prefix):
2891 # If not using refs/pending/heads/* at all, or target ref is already set
2892 # to pending, then push to the target ref directly.
2893 retcode, output = RunGitWithCode(
2894 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002895 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002896 else:
2897 # Cherry-pick the change on top of pending ref and then push it.
2898 assert branch.startswith('refs/'), branch
2899 assert pending_prefix[-1] == '/', pending_prefix
2900 pending_ref = pending_prefix + branch[len('refs/'):]
2901 retcode, output = PushToGitPending(remote, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002902 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002903 if retcode == 0:
2904 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002905 else:
2906 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002907 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002908 'svn', 'dcommit',
2909 '-C%s' % options.similarity,
2910 '--no-rebase', '--rmdir',
2911 ]
2912 if settings.GetForceHttpsCommitUrl():
2913 # Allow forcing https commit URLs for some projects that don't allow
2914 # committing to http URLs (like Google Code).
2915 remote_url = cl.GetGitSvnRemoteUrl()
2916 if urlparse.urlparse(remote_url).scheme == 'http':
2917 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002918 cmd_args.append('--commit-url=%s' % remote_url)
2919 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002920 if 'Committed r' in output:
2921 revision = re.match(
2922 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2923 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002924 finally:
2925 # And then swap back to the original branch and clean up.
2926 RunGit(['checkout', '-q', cl.GetBranch()])
2927 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002928 if base_has_submodules:
2929 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002930
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002931 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002932 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002933 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002934
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002935 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002936 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002937 try:
2938 revision = WaitForRealCommit(remote, revision, base_branch, branch)
2939 # We set pushed_to_pending to False, since it made it all the way to the
2940 # real ref.
2941 pushed_to_pending = False
2942 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002943 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002944
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002945 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002946 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002947 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002948 if not to_pending:
2949 if viewvc_url and revision:
2950 change_desc.append_footer(
2951 'Committed: %s%s' % (viewvc_url, revision))
2952 elif revision:
2953 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002954 print ('Closing issue '
2955 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002956 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002957 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002958 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002959 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00002960 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00002961 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002962 if options.bypass_hooks:
2963 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2964 else:
2965 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002966 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002967 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002968
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002969 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002970 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
2971 print 'The commit is in the pending queue (%s).' % pending_ref
2972 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00002973 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002974 'footer.' % branch)
2975
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002976 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2977 if os.path.isfile(hook):
2978 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002979
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002980 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002981
2982
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002983def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
2984 print
2985 print 'Waiting for commit to be landed on %s...' % real_ref
2986 print '(If you are impatient, you may Ctrl-C once without harm)'
2987 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
2988 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
2989
2990 loop = 0
2991 while True:
2992 sys.stdout.write('fetching (%d)... \r' % loop)
2993 sys.stdout.flush()
2994 loop += 1
2995
2996 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
2997 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2998 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
2999 for commit in commits.splitlines():
3000 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
3001 print 'Found commit on %s' % real_ref
3002 return commit
3003
3004 current_rev = to_rev
3005
3006
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003007def PushToGitPending(remote, pending_ref, upstream_ref):
3008 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
3009
3010 Returns:
3011 (retcode of last operation, output log of last operation).
3012 """
3013 assert pending_ref.startswith('refs/'), pending_ref
3014 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
3015 cherry = RunGit(['rev-parse', 'HEAD']).strip()
3016 code = 0
3017 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003018 max_attempts = 3
3019 attempts_left = max_attempts
3020 while attempts_left:
3021 if attempts_left != max_attempts:
3022 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
3023 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003024
3025 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003026 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003027 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003028 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003029 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003030 print 'Fetch failed with exit code %d.' % code
3031 if out.strip():
3032 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003033 continue
3034
3035 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003036 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003037 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003038 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003039 if code:
3040 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003041 'Your patch doesn\'t apply cleanly to ref \'%s\', '
3042 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003043 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
3044 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003045 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003046 return code, out
3047
3048 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003049 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003050 code, out = RunGitWithCode(
3051 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
3052 if code == 0:
3053 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003054 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003055 return code, out
3056
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003057 print 'Push failed with exit code %d.' % code
3058 if out.strip():
3059 print out.strip()
3060 if IsFatalPushFailure(out):
3061 print (
3062 'Fatal push error. Make sure your .netrc credentials and git '
3063 'user.email are correct and you have push access to the repo.')
3064 return code, out
3065
3066 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003067 return code, out
3068
3069
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003070def IsFatalPushFailure(push_stdout):
3071 """True if retrying push won't help."""
3072 return '(prohibited by Gerrit)' in push_stdout
3073
3074
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003075@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003076def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003077 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003078 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003079 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003080 # If it looks like previous commits were mirrored with git-svn.
3081 message = """This repository appears to be a git-svn mirror, but no
3082upstream SVN master is set. You probably need to run 'git auto-svn' once."""
3083 else:
3084 message = """This doesn't appear to be an SVN repository.
3085If your project has a true, writeable git repository, you probably want to run
3086'git cl land' instead.
3087If your project has a git mirror of an upstream SVN master, you probably need
3088to run 'git svn init'.
3089
3090Using the wrong command might cause your commit to appear to succeed, and the
3091review to be closed, without actually landing upstream. If you choose to
3092proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00003093 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00003094 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003095 return SendUpstream(parser, args, 'dcommit')
3096
3097
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003098@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003099def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003100 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003101 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003102 print('This appears to be an SVN repository.')
3103 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003104 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00003105 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003106 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003107
3108
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003109def ParseIssueNum(arg):
3110 """Parses the issue number from args if present otherwise returns None."""
3111 if re.match(r'\d+', arg):
3112 return arg
3113 if arg.startswith('http'):
3114 return re.sub(r'.*/(\d+)/?', r'\1', arg)
3115 return None
3116
3117
3118@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003119def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00003120 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003121 parser.add_option('-b', dest='newbranch',
3122 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003123 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003124 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003125 parser.add_option('-d', '--directory', action='store', metavar='DIR',
3126 help='Change to the directory DIR immediately, '
3127 'before doing anything else.')
3128 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003129 help='failed patches spew .rej files rather than '
3130 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003131 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
3132 help="don't commit after patch applies")
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003133
3134 group = optparse.OptionGroup(parser,
3135 """Options for continuing work on the current issue uploaded
3136from a different clone (e.g. different machine). Must be used independently from
3137the other options. No issue number should be specified, and the branch must have
3138an issue number associated with it""")
3139 group.add_option('--reapply', action='store_true',
3140 dest='reapply',
3141 help="""Reset the branch and reapply the issue.
3142CAUTION: This will undo any local changes in this branch""")
3143
3144 group.add_option('--pull', action='store_true', dest='pull',
3145 help="Performs a pull before reapplying.")
3146 parser.add_option_group(group)
3147
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003148 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003149 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003150 auth_config = auth.extract_auth_config_from_options(options)
3151
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003152 issue_arg = None
3153 if options.reapply :
3154 if len(args) > 0:
3155 parser.error("--reapply implies no additional arguments.")
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003156
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003157 cl = Changelist()
3158 issue_arg = cl.GetIssue()
3159 upstream = cl.GetUpstreamBranch()
3160 if upstream == None:
3161 parser.error("No upstream branch specified. Cannot reset branch")
3162
3163 RunGit(['reset', '--hard', upstream])
3164 if options.pull:
3165 RunGit(['pull'])
3166 else:
3167 if len(args) != 1:
3168 parser.error("Must specify issue number")
3169
3170 issue_arg = ParseIssueNum(args[0])
3171
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003172 # The patch URL works because ParseIssueNum won't do any substitution
3173 # as the re.sub pattern fails to match and just returns it.
3174 if issue_arg == None:
3175 parser.print_help()
3176 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003177
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003178 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003179 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003180 return 1
3181
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003182 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00003183 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003184
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003185 if options.newbranch:
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003186 if options.reapply:
3187 parser.error("--reapply excludes any option other than --pull")
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003188 if options.force:
3189 RunGit(['branch', '-D', options.newbranch],
3190 stderr=subprocess2.PIPE, error_ok=True)
3191 RunGit(['checkout', '-b', options.newbranch,
3192 Changelist().GetUpstreamBranch()])
3193
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003194 return PatchIssue(issue_arg, options.reject, options.nocommit,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003195 options.directory, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003196
3197
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003198def PatchIssue(issue_arg, reject, nocommit, directory, auth_config):
wychen@chromium.orga872e752015-04-28 23:42:18 +00003199 # PatchIssue should never be called with a dirty tree. It is up to the
3200 # caller to check this, but just in case we assert here since the
3201 # consequences of the caller not checking this could be dire.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003202 assert(not git_common.is_dirty_git_tree('apply'))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003203
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003204 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003205 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00003206 issue = int(issue_arg)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003207 cl = Changelist(issue=issue, auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003208 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00003209 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003210 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00003211 # Assume it's a URL to the patch. Default to https.
3212 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
kjellander@chromium.org44424542015-06-02 18:35:29 +00003213 match = re.match(r'(.*?)/download/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00003214 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003215 DieWithError('Must pass an issue ID or full URL for '
3216 '\'Download raw patch set\'')
kjellander@chromium.org44424542015-06-02 18:35:29 +00003217 issue = int(match.group(2))
3218 cl = Changelist(issue=issue, auth_config=auth_config)
3219 cl.rietveld_server = match.group(1)
3220 patchset = int(match.group(3))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00003221 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003222
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003223 # Switch up to the top-level directory, if necessary, in preparation for
3224 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003225 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003226 if top:
3227 os.chdir(top)
3228
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003229 # Git patches have a/ at the beginning of source paths. We strip that out
3230 # with a sed script rather than the -p flag to patch so we can feed either
3231 # Git or svn-style patches into the same apply command.
3232 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003233 try:
3234 patch_data = subprocess2.check_output(
3235 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
3236 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003237 DieWithError('Git patch mungling failed.')
3238 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003239
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003240 # We use "git apply" to apply the patch instead of "patch" so that we can
3241 # pick up file adds.
3242 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003243 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003244 if directory:
3245 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003246 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003247 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003248 elif IsGitVersionAtLeast('1.7.12'):
3249 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003250 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003251 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003252 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003253 except subprocess2.CalledProcessError:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003254 print 'Failed to apply the patch'
3255 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003256
3257 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003258 if not nocommit:
wychen@chromium.org5b3bebb2015-05-28 21:41:43 +00003259 RunGit(['commit', '-m', (cl.GetDescription() + '\n\n' +
3260 'patch from issue %(i)s at patchset '
carlosk@chromium.org71284d92014-11-14 18:12:50 +00003261 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
3262 % {'i': issue, 'p': patchset})])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003263 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003264 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00003265 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00003266 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003267 else:
3268 print "Patch applied to index."
3269 return 0
3270
3271
3272def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003273 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003274 # Provide a wrapper for git svn rebase to help avoid accidental
3275 # git svn dcommit.
3276 # It's the only command that doesn't use parser at all since we just defer
3277 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003278
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003279 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003280
3281
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003282def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003283 """Fetches the tree status and returns either 'open', 'closed',
3284 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003285 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003286 if url:
3287 status = urllib2.urlopen(url).read().lower()
3288 if status.find('closed') != -1 or status == '0':
3289 return 'closed'
3290 elif status.find('open') != -1 or status == '1':
3291 return 'open'
3292 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003293 return 'unset'
3294
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003295
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003296def GetTreeStatusReason():
3297 """Fetches the tree status from a json url and returns the message
3298 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00003299 url = settings.GetTreeStatusUrl()
3300 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003301 connection = urllib2.urlopen(json_url)
3302 status = json.loads(connection.read())
3303 connection.close()
3304 return status['message']
3305
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003306
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003307def GetBuilderMaster(bot_list):
3308 """For a given builder, fetch the master from AE if available."""
3309 map_url = 'https://builders-map.appspot.com/'
3310 try:
3311 master_map = json.load(urllib2.urlopen(map_url))
3312 except urllib2.URLError as e:
3313 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
3314 (map_url, e))
3315 except ValueError as e:
3316 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
3317 if not master_map:
3318 return None, 'Failed to build master map.'
3319
3320 result_master = ''
3321 for bot in bot_list:
3322 builder = bot.split(':', 1)[0]
3323 master_list = master_map.get(builder, [])
3324 if not master_list:
3325 return None, ('No matching master for builder %s.' % builder)
3326 elif len(master_list) > 1:
3327 return None, ('The builder name %s exists in multiple masters %s.' %
3328 (builder, master_list))
3329 else:
3330 cur_master = master_list[0]
3331 if not result_master:
3332 result_master = cur_master
3333 elif result_master != cur_master:
3334 return None, 'The builders do not belong to the same master.'
3335 return result_master, None
3336
3337
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003338def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003339 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003340 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003341 status = GetTreeStatus()
3342 if 'unset' == status:
3343 print 'You must configure your tree status URL by running "git cl config".'
3344 return 2
3345
3346 print "The tree is %s" % status
3347 print
3348 print GetTreeStatusReason()
3349 if status != 'open':
3350 return 1
3351 return 0
3352
3353
maruel@chromium.org15192402012-09-06 12:38:29 +00003354def CMDtry(parser, args):
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003355 """Triggers a try job through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00003356 group = optparse.OptionGroup(parser, "Try job options")
3357 group.add_option(
3358 "-b", "--bot", action="append",
3359 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
3360 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003361 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00003362 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003363 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00003364 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003365 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00003366 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003367 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003368 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003369 "-r", "--revision",
3370 help="Revision to use for the try job; default: the "
3371 "revision will be determined by the try server; see "
3372 "its waterfall for more info")
3373 group.add_option(
3374 "-c", "--clobber", action="store_true", default=False,
3375 help="Force a clobber before building; e.g. don't do an "
3376 "incremental build")
3377 group.add_option(
3378 "--project",
3379 help="Override which project to use. Projects are defined "
3380 "server-side to define what default bot set to use")
3381 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00003382 "-p", "--property", dest="properties", action="append", default=[],
3383 help="Specify generic properties in the form -p key1=value1 -p "
3384 "key2=value2 etc (buildbucket only). The value will be treated as "
3385 "json if decodable, or as string otherwise.")
3386 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003387 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003388 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003389 "--use-rietveld", action="store_true", default=False,
3390 help="Use Rietveld to trigger try jobs.")
3391 group.add_option(
3392 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3393 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00003394 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003395 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00003396 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003397 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00003398
machenbach@chromium.org45453142015-09-15 08:45:22 +00003399 if options.use_rietveld and options.properties:
3400 parser.error('Properties can only be specified with buildbucket')
3401
3402 # Make sure that all properties are prop=value pairs.
3403 bad_params = [x for x in options.properties if '=' not in x]
3404 if bad_params:
3405 parser.error('Got properties with missing "=": %s' % bad_params)
3406
maruel@chromium.org15192402012-09-06 12:38:29 +00003407 if args:
3408 parser.error('Unknown arguments: %s' % args)
3409
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003410 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00003411 if not cl.GetIssue():
3412 parser.error('Need to upload first')
3413
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003414 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00003415 if props.get('closed'):
3416 parser.error('Cannot send tryjobs for a closed CL')
3417
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003418 if props.get('private'):
3419 parser.error('Cannot use trybots with private issue')
3420
maruel@chromium.org15192402012-09-06 12:38:29 +00003421 if not options.name:
3422 options.name = cl.GetBranch()
3423
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003424 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003425 options.master, err_msg = GetBuilderMaster(options.bot)
3426 if err_msg:
3427 parser.error('Tryserver master cannot be found because: %s\n'
3428 'Please manually specify the tryserver master'
3429 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003430
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003431 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003432 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003433 if not options.bot:
3434 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00003435
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003436 # Get try masters from PRESUBMIT.py files.
3437 masters = presubmit_support.DoGetTryMasters(
3438 change,
3439 change.LocalPaths(),
3440 settings.GetRoot(),
3441 None,
3442 None,
3443 options.verbose,
3444 sys.stdout)
3445 if masters:
3446 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00003447
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003448 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
3449 options.bot = presubmit_support.DoGetTrySlaves(
3450 change,
3451 change.LocalPaths(),
3452 settings.GetRoot(),
3453 None,
3454 None,
3455 options.verbose,
3456 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00003457
3458 if not options.bot:
3459 # Get try masters from cq.cfg if any.
3460 # TODO(tandrii): some (but very few) projects store cq.cfg in different
3461 # location.
3462 cq_cfg = os.path.join(change.RepositoryRoot(),
3463 'infra', 'config', 'cq.cfg')
3464 if os.path.exists(cq_cfg):
3465 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00003466 cq_masters = commit_queue.get_master_builder_map(
3467 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00003468 for master, builders in cq_masters.iteritems():
3469 for builder in builders:
3470 # Skip presubmit builders, because these will fail without LGTM.
3471 if 'presubmit' not in builder.lower():
3472 masters.setdefault(master, {})[builder] = ['defaulttests']
3473 if masters:
3474 return masters
3475
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003476 if not options.bot:
3477 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00003478
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003479 builders_and_tests = {}
3480 # TODO(machenbach): The old style command-line options don't support
3481 # multiple try masters yet.
3482 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
3483 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
3484
3485 for bot in old_style:
3486 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003487 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003488 elif ',' in bot:
3489 parser.error('Specify one bot per --bot flag')
3490 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00003491 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003492
3493 for bot, tests in new_style:
3494 builders_and_tests.setdefault(bot, []).extend(tests)
3495
3496 # Return a master map with one master to be backwards compatible. The
3497 # master name defaults to an empty string, which will cause the master
3498 # not to be set on rietveld (deprecated).
3499 return {options.master: builders_and_tests}
3500
3501 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00003502
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003503 for builders in masters.itervalues():
3504 if any('triggered' in b for b in builders):
3505 print >> sys.stderr, (
3506 'ERROR You are trying to send a job to a triggered bot. This type of'
3507 ' bot requires an\ninitial job from a parent (usually a builder). '
3508 'Instead send your job to the parent.\n'
3509 'Bot list: %s' % builders)
3510 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00003511
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00003512 patchset = cl.GetMostRecentPatchset()
3513 if patchset and patchset != cl.GetPatchset():
3514 print(
3515 '\nWARNING Mismatch between local config and server. Did a previous '
3516 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3517 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003518 if options.luci:
3519 trigger_luci_job(cl, masters, options)
3520 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003521 try:
3522 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
3523 except BuildbucketResponseException as ex:
3524 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00003525 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003526 except Exception as e:
3527 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3528 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
3529 e, stacktrace)
3530 return 1
3531 else:
3532 try:
3533 cl.RpcServer().trigger_distributed_try_jobs(
3534 cl.GetIssue(), patchset, options.name, options.clobber,
3535 options.revision, masters)
3536 except urllib2.HTTPError as e:
3537 if e.code == 404:
3538 print('404 from rietveld; '
3539 'did you mean to use "git try" instead of "git cl try"?')
3540 return 1
3541 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003542
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003543 for (master, builders) in sorted(masters.iteritems()):
3544 if master:
3545 print 'Master: %s' % master
3546 length = max(len(builder) for builder in builders)
3547 for builder in sorted(builders):
3548 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00003549 return 0
3550
3551
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00003552def CMDtry_results(parser, args):
3553 group = optparse.OptionGroup(parser, "Try job results options")
3554 group.add_option(
3555 "-p", "--patchset", type=int, help="patchset number if not current.")
3556 group.add_option(
3557 "--print-master", action='store_true', help="print master name as well")
3558 group.add_option(
3559 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3560 help="Host of buildbucket. The default host is %default.")
3561 parser.add_option_group(group)
3562 auth.add_auth_options(parser)
3563 options, args = parser.parse_args(args)
3564 if args:
3565 parser.error('Unrecognized args: %s' % ' '.join(args))
3566
3567 auth_config = auth.extract_auth_config_from_options(options)
3568 cl = Changelist(auth_config=auth_config)
3569 if not cl.GetIssue():
3570 parser.error('Need to upload first')
3571
3572 if not options.patchset:
3573 options.patchset = cl.GetMostRecentPatchset()
3574 if options.patchset and options.patchset != cl.GetPatchset():
3575 print(
3576 '\nWARNING Mismatch between local config and server. Did a previous '
3577 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3578 'Continuing using\npatchset %s.\n' % options.patchset)
3579 try:
3580 jobs = fetch_try_jobs(auth_config, cl, options)
3581 except BuildbucketResponseException as ex:
3582 print 'Buildbucket error: %s' % ex
3583 return 1
3584 except Exception as e:
3585 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3586 print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
3587 e, stacktrace)
3588 return 1
3589 print_tryjobs(options, jobs)
3590 return 0
3591
3592
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003593@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003594def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003595 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003596 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003597 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003598 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003599
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003600 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003601 if args:
3602 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003603 branch = cl.GetBranch()
3604 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003605 cl = Changelist()
3606 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003607
3608 # Clear configured merge-base, if there is one.
3609 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003610 else:
3611 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003612 return 0
3613
3614
thestig@chromium.org00858c82013-12-02 23:08:03 +00003615def CMDweb(parser, args):
3616 """Opens the current CL in the web browser."""
3617 _, args = parser.parse_args(args)
3618 if args:
3619 parser.error('Unrecognized args: %s' % ' '.join(args))
3620
3621 issue_url = Changelist().GetIssueURL()
3622 if not issue_url:
3623 print >> sys.stderr, 'ERROR No issue to open'
3624 return 1
3625
3626 webbrowser.open(issue_url)
3627 return 0
3628
3629
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003630def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003631 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003632 auth.add_auth_options(parser)
3633 options, args = parser.parse_args(args)
3634 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003635 if args:
3636 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003637 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003638 props = cl.GetIssueProperties()
3639 if props.get('private'):
3640 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003641 cl.SetFlag('commit', '1')
3642 return 0
3643
3644
groby@chromium.org411034a2013-02-26 15:12:01 +00003645def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003646 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003647 auth.add_auth_options(parser)
3648 options, args = parser.parse_args(args)
3649 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00003650 if args:
3651 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003652 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00003653 # Ensure there actually is an issue to close.
3654 cl.GetDescription()
3655 cl.CloseIssue()
3656 return 0
3657
3658
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003659def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003660 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003661 auth.add_auth_options(parser)
3662 options, args = parser.parse_args(args)
3663 auth_config = auth.extract_auth_config_from_options(options)
3664 if args:
3665 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003666
3667 # Uncommitted (staged and unstaged) changes will be destroyed by
3668 # "git reset --hard" if there are merging conflicts in PatchIssue().
3669 # Staged changes would be committed along with the patch from last
3670 # upload, hence counted toward the "last upload" side in the final
3671 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003672 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003673 return 1
3674
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003675 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003676 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003677 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003678 if not issue:
3679 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003680 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003681 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003682
3683 # Create a new branch based on the merge-base
3684 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
3685 try:
3686 # Patch in the latest changes from rietveld.
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003687 rtn = PatchIssue(issue, False, False, None, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003688 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003689 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003690 return rtn
3691
wychen@chromium.org06928532015-02-03 02:11:29 +00003692 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003693 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00003694 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003695 finally:
3696 RunGit(['checkout', '-q', branch])
3697 RunGit(['branch', '-D', TMP_BRANCH])
3698
3699 return 0
3700
3701
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003702def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003703 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003704 parser.add_option(
3705 '--no-color',
3706 action='store_true',
3707 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003708 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003709 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003710 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003711
3712 author = RunGit(['config', 'user.email']).strip() or None
3713
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003714 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003715
3716 if args:
3717 if len(args) > 1:
3718 parser.error('Unknown args')
3719 base_branch = args[0]
3720 else:
3721 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003722 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003723
3724 change = cl.GetChange(base_branch, None)
3725 return owners_finder.OwnersFinder(
3726 [f.LocalPath() for f in
3727 cl.GetChange(base_branch, None).AffectedFiles()],
3728 change.RepositoryRoot(), author,
3729 fopen=file, os_path=os.path, glob=glob.glob,
3730 disable_color=options.no_color).run()
3731
3732
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003733def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003734 """Generates a diff command."""
3735 # Generate diff for the current branch's changes.
3736 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
3737 upstream_commit, '--' ]
3738
3739 if args:
3740 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003741 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003742 diff_cmd.append(arg)
3743 else:
3744 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003745
3746 return diff_cmd
3747
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003748def MatchingFileType(file_name, extensions):
3749 """Returns true if the file name ends with one of the given extensions."""
3750 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003751
enne@chromium.org555cfe42014-01-29 18:21:39 +00003752@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003753def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003754 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00003755 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00003756 GN_EXTS = ['.gn', '.gni']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003757 parser.add_option('--full', action='store_true',
3758 help='Reformat the full content of all touched files')
3759 parser.add_option('--dry-run', action='store_true',
3760 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003761 parser.add_option('--python', action='store_true',
3762 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003763 parser.add_option('--diff', action='store_true',
3764 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003765 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003766
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003767 # git diff generates paths against the root of the repository. Change
3768 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003769 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003770 if rel_base_path:
3771 os.chdir(rel_base_path)
3772
digit@chromium.org29e47272013-05-17 17:01:46 +00003773 # Grab the merge-base commit, i.e. the upstream commit of the current
3774 # branch when it was created or the last time it was rebased. This is
3775 # to cover the case where the user may have called "git fetch origin",
3776 # moving the origin branch to a newer commit, but hasn't rebased yet.
3777 upstream_commit = None
3778 cl = Changelist()
3779 upstream_branch = cl.GetUpstreamBranch()
3780 if upstream_branch:
3781 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
3782 upstream_commit = upstream_commit.strip()
3783
3784 if not upstream_commit:
3785 DieWithError('Could not find base commit for this branch. '
3786 'Are you in detached state?')
3787
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003788 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
3789 diff_output = RunGit(changed_files_cmd)
3790 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00003791 # Filter out files deleted by this CL
3792 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003793
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003794 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
3795 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
3796 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00003797 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00003798
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003799 top_dir = os.path.normpath(
3800 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
3801
3802 # Locate the clang-format binary in the checkout
3803 try:
3804 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
3805 except clang_format.NotFoundError, e:
3806 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00003807
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003808 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
3809 # formatted. This is used to block during the presubmit.
3810 return_value = 0
3811
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003812 if clang_diff_files:
3813 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003814 cmd = [clang_format_tool]
3815 if not opts.dry_run and not opts.diff:
3816 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003817 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003818 if opts.diff:
3819 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003820 else:
3821 env = os.environ.copy()
3822 env['PATH'] = str(os.path.dirname(clang_format_tool))
3823 try:
3824 script = clang_format.FindClangFormatScriptInChromiumTree(
3825 'clang-format-diff.py')
3826 except clang_format.NotFoundError, e:
3827 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003828
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003829 cmd = [sys.executable, script, '-p0']
3830 if not opts.dry_run and not opts.diff:
3831 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003832
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003833 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
3834 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003835
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003836 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
3837 if opts.diff:
3838 sys.stdout.write(stdout)
3839 if opts.dry_run and len(stdout) > 0:
3840 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003841
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003842 # Similar code to above, but using yapf on .py files rather than clang-format
3843 # on C/C++ files
3844 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003845 yapf_tool = gclient_utils.FindExecutable('yapf')
3846 if yapf_tool is None:
3847 DieWithError('yapf not found in PATH')
3848
3849 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003850 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003851 cmd = [yapf_tool]
3852 if not opts.dry_run and not opts.diff:
3853 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003854 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003855 if opts.diff:
3856 sys.stdout.write(stdout)
3857 else:
3858 # TODO(sbc): yapf --lines mode still has some issues.
3859 # https://github.com/google/yapf/issues/154
3860 DieWithError('--python currently only works with --full')
3861
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003862 # Dart's formatter does not have the nice property of only operating on
3863 # modified chunks, so hard code full.
3864 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003865 try:
3866 command = [dart_format.FindDartFmtToolInChromiumTree()]
3867 if not opts.dry_run and not opts.diff:
3868 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003869 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003870
ppi@chromium.org6593d932016-03-03 15:41:15 +00003871 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003872 if opts.dry_run and stdout:
3873 return_value = 2
3874 except dart_format.NotFoundError as e:
erikcorry@chromium.org3e445022015-12-17 09:07:26 +00003875 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
3876 'found in this checkout. Files in other languages are still ' +
3877 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003878
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00003879 # Format GN build files. Always run on full build files for canonical form.
3880 if gn_diff_files:
3881 cmd = ['gn', 'format']
3882 if not opts.dry_run and not opts.diff:
3883 cmd.append('--in-place')
3884 for gn_diff_file in gn_diff_files:
3885 stdout = RunCommand(cmd + [gn_diff_file], cwd=top_dir)
3886 if opts.diff:
3887 sys.stdout.write(stdout)
3888
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003889 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003890
3891
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00003892@subcommand.usage('<codereview url or issue id>')
3893def CMDcheckout(parser, args):
3894 """Checks out a branch associated with a given Rietveld issue."""
3895 _, args = parser.parse_args(args)
3896
3897 if len(args) != 1:
3898 parser.print_help()
3899 return 1
3900
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003901 target_issue = ParseIssueNum(args[0])
3902 if target_issue == None:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00003903 parser.print_help()
3904 return 1
3905
3906 key_and_issues = [x.split() for x in RunGit(
3907 ['config', '--local', '--get-regexp', r'branch\..*\.rietveldissue'])
3908 .splitlines()]
3909 branches = []
3910 for key, issue in key_and_issues:
3911 if issue == target_issue:
3912 branches.append(re.sub(r'branch\.(.*)\.rietveldissue', r'\1', key))
3913
3914 if len(branches) == 0:
3915 print 'No branch found for issue %s.' % target_issue
3916 return 1
3917 if len(branches) == 1:
3918 RunGit(['checkout', branches[0]])
3919 else:
3920 print 'Multiple branches match issue %s:' % target_issue
3921 for i in range(len(branches)):
3922 print '%d: %s' % (i, branches[i])
3923 which = raw_input('Choose by index: ')
3924 try:
3925 RunGit(['checkout', branches[int(which)]])
3926 except (IndexError, ValueError):
3927 print 'Invalid selection, not checking out any branch.'
3928 return 1
3929
3930 return 0
3931
3932
maruel@chromium.org29404b52014-09-08 22:58:00 +00003933def CMDlol(parser, args):
3934 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00003935 print zlib.decompress(base64.b64decode(
3936 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
3937 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
3938 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
3939 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00003940 return 0
3941
3942
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003943class OptionParser(optparse.OptionParser):
3944 """Creates the option parse and add --verbose support."""
3945 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003946 optparse.OptionParser.__init__(
3947 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003948 self.add_option(
3949 '-v', '--verbose', action='count', default=0,
3950 help='Use 2 times for more debugging info')
3951
3952 def parse_args(self, args=None, values=None):
3953 options, args = optparse.OptionParser.parse_args(self, args, values)
3954 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
3955 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
3956 return options, args
3957
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003958
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003959def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00003960 if sys.hexversion < 0x02060000:
3961 print >> sys.stderr, (
3962 '\nYour python version %s is unsupported, please upgrade.\n' %
3963 sys.version.split(' ', 1)[0])
3964 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003965
maruel@chromium.orgddd59412011-11-30 14:20:38 +00003966 # Reload settings.
3967 global settings
3968 settings = Settings()
3969
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003970 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003971 dispatcher = subcommand.CommandDispatcher(__name__)
3972 try:
3973 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003974 except auth.AuthenticationError as e:
3975 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003976 except urllib2.HTTPError, e:
3977 if e.code != 500:
3978 raise
3979 DieWithError(
3980 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
3981 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00003982 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003983
3984
3985if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003986 # These affect sys.stdout so do it outside of main() to simplify mocks in
3987 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00003988 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003989 colorama.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00003990 try:
3991 sys.exit(main(sys.argv[1:]))
3992 except KeyboardInterrupt:
3993 sys.stderr.write('interrupted\n')
3994 sys.exit(1)