blob: b3d1d2eb9daeb25e77dbf9a4c9aa5ac83011d6ce [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
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00008"""A git-command for integrating reviews on Rietveld and Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
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
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000050import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000051import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000052import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000053import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000054import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000055import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000056import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000057import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000058import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000059import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000060import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000061import watchlists
62
maruel@chromium.org0633fb42013-08-16 20:06:14 +000063__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000064
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000065DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000066POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000067DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000068GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
rmistry@google.comc68112d2015-03-03 12:48:06 +000069REFS_THAT_ALIAS_TO_OTHER_REFS = {
70 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
71 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
72}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000073
thestig@chromium.org44202a22014-03-11 19:22:18 +000074# Valid extensions for files we want to lint.
75DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
76DEFAULT_LINT_IGNORE_REGEX = r"$^"
77
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000078# Shortcut since it quickly becomes redundant.
79Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000080
maruel@chromium.orgddd59412011-11-30 14:20:38 +000081# Initialized in main()
82settings = None
83
84
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000085def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000086 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000087 sys.exit(1)
88
89
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000090def GetNoGitPagerEnv():
91 env = os.environ.copy()
92 # 'cat' is a magical git string that disables pagers on all platforms.
93 env['GIT_PAGER'] = 'cat'
94 return env
95
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000096
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000097def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000098 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000099 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000100 except subprocess2.CalledProcessError as e:
101 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000102 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000103 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000104 'Command "%s" failed.\n%s' % (
105 ' '.join(args), error_message or e.stdout or ''))
106 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000107
108
109def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000110 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000111 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000112
113
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000114def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000115 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000116 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000117 if suppress_stderr:
118 stderr = subprocess2.VOID
119 else:
120 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000121 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000122 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000123 stdout=subprocess2.PIPE,
124 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000125 return code, out[0]
126 except ValueError:
127 # When the subprocess fails, it returns None. That triggers a ValueError
128 # when trying to unpack the return value into (out, code).
129 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000130
131
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000132def RunGitSilent(args):
133 """Returns stdout, suppresses stderr and ingores the return code."""
134 return RunGitWithCode(args, suppress_stderr=True)[1]
135
136
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000137def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000138 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000139 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000140 return (version.startswith(prefix) and
141 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000142
143
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000144def BranchExists(branch):
145 """Return True if specified branch exists."""
146 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
147 suppress_stderr=True)
148 return not code
149
150
maruel@chromium.org90541732011-04-01 17:54:18 +0000151def ask_for_data(prompt):
152 try:
153 return raw_input(prompt)
154 except KeyboardInterrupt:
155 # Hide the exception.
156 sys.exit(1)
157
158
iannucci@chromium.org79540052012-10-19 23:15:26 +0000159def git_set_branch_value(key, value):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000160 branch = GetCurrentBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000161 if not branch:
162 return
163
164 cmd = ['config']
165 if isinstance(value, int):
166 cmd.append('--int')
167 git_key = 'branch.%s.%s' % (branch, key)
168 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000169
170
171def git_get_branch_default(key, default):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000172 branch = GetCurrentBranch()
iannucci@chromium.org79540052012-10-19 23:15:26 +0000173 if branch:
174 git_key = 'branch.%s.%s' % (branch, key)
175 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
176 try:
177 return int(stdout.strip())
178 except ValueError:
179 pass
180 return default
181
182
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000183def add_git_similarity(parser):
184 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000185 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000186 help='Sets the percentage that a pair of files need to match in order to'
187 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000188 parser.add_option(
189 '--find-copies', action='store_true',
190 help='Allows git to look for copies.')
191 parser.add_option(
192 '--no-find-copies', action='store_false', dest='find_copies',
193 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000194
195 old_parser_args = parser.parse_args
196 def Parse(args):
197 options, args = old_parser_args(args)
198
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000199 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000200 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000201 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000202 print('Note: Saving similarity of %d%% in git config.'
203 % options.similarity)
204 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000205
iannucci@chromium.org79540052012-10-19 23:15:26 +0000206 options.similarity = max(0, min(options.similarity, 100))
207
208 if options.find_copies is None:
209 options.find_copies = bool(
210 git_get_branch_default('git-find-copies', True))
211 else:
212 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000213
214 print('Using %d%% similarity for rename/copy detection. '
215 'Override with --similarity.' % options.similarity)
216
217 return options, args
218 parser.parse_args = Parse
219
220
machenbach@chromium.org45453142015-09-15 08:45:22 +0000221def _get_properties_from_options(options):
222 properties = dict(x.split('=', 1) for x in options.properties)
223 for key, val in properties.iteritems():
224 try:
225 properties[key] = json.loads(val)
226 except ValueError:
227 pass # If a value couldn't be evaluated, treat it as a string.
228 return properties
229
230
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000231def _prefix_master(master):
232 """Convert user-specified master name to full master name.
233
234 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
235 name, while the developers always use shortened master name
236 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
237 function does the conversion for buildbucket migration.
238 """
239 prefix = 'master.'
240 if master.startswith(prefix):
241 return master
242 return '%s%s' % (prefix, master)
243
244
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000245def _buildbucket_retry(operation_name, http, *args, **kwargs):
246 """Retries requests to buildbucket service and returns parsed json content."""
247 try_count = 0
248 while True:
249 response, content = http.request(*args, **kwargs)
250 try:
251 content_json = json.loads(content)
252 except ValueError:
253 content_json = None
254
255 # Buildbucket could return an error even if status==200.
256 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000257 error = content_json.get('error')
258 if error.get('code') == 403:
259 raise BuildbucketResponseException(
260 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000261 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000262 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000263 raise BuildbucketResponseException(msg)
264
265 if response.status == 200:
266 if not content_json:
267 raise BuildbucketResponseException(
268 'Buildbucket returns invalid json content: %s.\n'
269 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
270 content)
271 return content_json
272 if response.status < 500 or try_count >= 2:
273 raise httplib2.HttpLib2Error(content)
274
275 # status >= 500 means transient failures.
276 logging.debug('Transient errors when %s. Will retry.', operation_name)
277 time.sleep(0.5 + 1.5*try_count)
278 try_count += 1
279 assert False, 'unreachable'
280
281
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000282def trigger_luci_job(changelist, masters, options):
283 """Send a job to run on LUCI."""
284 issue_props = changelist.GetIssueProperties()
285 issue = changelist.GetIssue()
286 patchset = changelist.GetMostRecentPatchset()
287 for builders_and_tests in sorted(masters.itervalues()):
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000288 # TODO(hinoka et al): add support for other properties.
289 # Currently, this completely ignores testfilter and other properties.
290 for builder in sorted(builders_and_tests):
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000291 luci_trigger.trigger(
292 builder, 'HEAD', issue, patchset, issue_props['project'])
293
294
machenbach@chromium.org45453142015-09-15 08:45:22 +0000295def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000296 rietveld_url = settings.GetDefaultServerUrl()
297 rietveld_host = urlparse.urlparse(rietveld_url).hostname
298 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
299 http = authenticator.authorize(httplib2.Http())
300 http.force_exception_to_status_code = True
301 issue_props = changelist.GetIssueProperties()
302 issue = changelist.GetIssue()
303 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000304 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000305
306 buildbucket_put_url = (
307 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000308 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000309 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
310 hostname=rietveld_host,
311 issue=issue,
312 patch=patchset)
313
314 batch_req_body = {'builds': []}
315 print_text = []
316 print_text.append('Tried jobs on:')
317 for master, builders_and_tests in sorted(masters.iteritems()):
318 print_text.append('Master: %s' % master)
319 bucket = _prefix_master(master)
320 for builder, tests in sorted(builders_and_tests.iteritems()):
321 print_text.append(' %s: %s' % (builder, tests))
322 parameters = {
323 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000324 'changes': [{
325 'author': {'email': issue_props['owner_email']},
326 'revision': options.revision,
327 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000328 'properties': {
329 'category': category,
330 'issue': issue,
331 'master': master,
332 'patch_project': issue_props['project'],
333 'patch_storage': 'rietveld',
334 'patchset': patchset,
335 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000336 'rietveld': rietveld_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000337 },
338 }
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000339 if tests:
340 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000341 if properties:
342 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000343 if options.clobber:
344 parameters['properties']['clobber'] = True
345 batch_req_body['builds'].append(
346 {
347 'bucket': bucket,
348 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000349 'client_operation_id': str(uuid.uuid4()),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000350 'tags': ['builder:%s' % builder,
351 'buildset:%s' % buildset,
352 'master:%s' % master,
353 'user_agent:git_cl_try']
354 }
355 )
356
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000357 _buildbucket_retry(
358 'triggering tryjobs',
359 http,
360 buildbucket_put_url,
361 'PUT',
362 body=json.dumps(batch_req_body),
363 headers={'Content-Type': 'application/json'}
364 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000365 print_text.append('To see results here, run: git cl try-results')
366 print_text.append('To see results in browser, run: git cl web')
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000367 print '\n'.join(print_text)
kjellander@chromium.org44424542015-06-02 18:35:29 +0000368
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000369
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000370def fetch_try_jobs(auth_config, changelist, options):
371 """Fetches tryjobs from buildbucket.
372
373 Returns a map from build id to build info as json dictionary.
374 """
375 rietveld_url = settings.GetDefaultServerUrl()
376 rietveld_host = urlparse.urlparse(rietveld_url).hostname
377 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
378 if authenticator.has_cached_credentials():
379 http = authenticator.authorize(httplib2.Http())
380 else:
381 print ('Warning: Some results might be missing because %s' %
382 # Get the message on how to login.
383 auth.LoginRequiredError(rietveld_host).message)
384 http = httplib2.Http()
385
386 http.force_exception_to_status_code = True
387
388 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
389 hostname=rietveld_host,
390 issue=changelist.GetIssue(),
391 patch=options.patchset)
392 params = {'tag': 'buildset:%s' % buildset}
393
394 builds = {}
395 while True:
396 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
397 hostname=options.buildbucket_host,
398 params=urllib.urlencode(params))
399 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
400 for build in content.get('builds', []):
401 builds[build['id']] = build
402 if 'next_cursor' in content:
403 params['start_cursor'] = content['next_cursor']
404 else:
405 break
406 return builds
407
408
409def print_tryjobs(options, builds):
410 """Prints nicely result of fetch_try_jobs."""
411 if not builds:
412 print 'No tryjobs scheduled'
413 return
414
415 # Make a copy, because we'll be modifying builds dictionary.
416 builds = builds.copy()
417 builder_names_cache = {}
418
419 def get_builder(b):
420 try:
421 return builder_names_cache[b['id']]
422 except KeyError:
423 try:
424 parameters = json.loads(b['parameters_json'])
425 name = parameters['builder_name']
426 except (ValueError, KeyError) as error:
427 print 'WARNING: failed to get builder name for build %s: %s' % (
428 b['id'], error)
429 name = None
430 builder_names_cache[b['id']] = name
431 return name
432
433 def get_bucket(b):
434 bucket = b['bucket']
435 if bucket.startswith('master.'):
436 return bucket[len('master.'):]
437 return bucket
438
439 if options.print_master:
440 name_fmt = '%%-%ds %%-%ds' % (
441 max(len(str(get_bucket(b))) for b in builds.itervalues()),
442 max(len(str(get_builder(b))) for b in builds.itervalues()))
443 def get_name(b):
444 return name_fmt % (get_bucket(b), get_builder(b))
445 else:
446 name_fmt = '%%-%ds' % (
447 max(len(str(get_builder(b))) for b in builds.itervalues()))
448 def get_name(b):
449 return name_fmt % get_builder(b)
450
451 def sort_key(b):
452 return b['status'], b.get('result'), get_name(b), b.get('url')
453
454 def pop(title, f, color=None, **kwargs):
455 """Pop matching builds from `builds` dict and print them."""
456
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000457 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000458 colorize = str
459 else:
460 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
461
462 result = []
463 for b in builds.values():
464 if all(b.get(k) == v for k, v in kwargs.iteritems()):
465 builds.pop(b['id'])
466 result.append(b)
467 if result:
468 print colorize(title)
469 for b in sorted(result, key=sort_key):
470 print ' ', colorize('\t'.join(map(str, f(b))))
471
472 total = len(builds)
473 pop(status='COMPLETED', result='SUCCESS',
474 title='Successes:', color=Fore.GREEN,
475 f=lambda b: (get_name(b), b.get('url')))
476 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
477 title='Infra Failures:', color=Fore.MAGENTA,
478 f=lambda b: (get_name(b), b.get('url')))
479 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
480 title='Failures:', color=Fore.RED,
481 f=lambda b: (get_name(b), b.get('url')))
482 pop(status='COMPLETED', result='CANCELED',
483 title='Canceled:', color=Fore.MAGENTA,
484 f=lambda b: (get_name(b),))
485 pop(status='COMPLETED', result='FAILURE',
486 failure_reason='INVALID_BUILD_DEFINITION',
487 title='Wrong master/builder name:', color=Fore.MAGENTA,
488 f=lambda b: (get_name(b),))
489 pop(status='COMPLETED', result='FAILURE',
490 title='Other failures:',
491 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
492 pop(status='COMPLETED',
493 title='Other finished:',
494 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
495 pop(status='STARTED',
496 title='Started:', color=Fore.YELLOW,
497 f=lambda b: (get_name(b), b.get('url')))
498 pop(status='SCHEDULED',
499 title='Scheduled:',
500 f=lambda b: (get_name(b), 'id=%s' % b['id']))
501 # The last section is just in case buildbucket API changes OR there is a bug.
502 pop(title='Other:',
503 f=lambda b: (get_name(b), 'id=%s' % b['id']))
504 assert len(builds) == 0
505 print 'Total: %d tryjobs' % total
506
507
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000508def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
509 """Return the corresponding git ref if |base_url| together with |glob_spec|
510 matches the full |url|.
511
512 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
513 """
514 fetch_suburl, as_ref = glob_spec.split(':')
515 if allow_wildcards:
516 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
517 if glob_match:
518 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
519 # "branches/{472,597,648}/src:refs/remotes/svn/*".
520 branch_re = re.escape(base_url)
521 if glob_match.group(1):
522 branch_re += '/' + re.escape(glob_match.group(1))
523 wildcard = glob_match.group(2)
524 if wildcard == '*':
525 branch_re += '([^/]*)'
526 else:
527 # Escape and replace surrounding braces with parentheses and commas
528 # with pipe symbols.
529 wildcard = re.escape(wildcard)
530 wildcard = re.sub('^\\\\{', '(', wildcard)
531 wildcard = re.sub('\\\\,', '|', wildcard)
532 wildcard = re.sub('\\\\}$', ')', wildcard)
533 branch_re += wildcard
534 if glob_match.group(3):
535 branch_re += re.escape(glob_match.group(3))
536 match = re.match(branch_re, url)
537 if match:
538 return re.sub('\*$', match.group(1), as_ref)
539
540 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
541 if fetch_suburl:
542 full_url = base_url + '/' + fetch_suburl
543 else:
544 full_url = base_url
545 if full_url == url:
546 return as_ref
547 return None
548
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000549
iannucci@chromium.org79540052012-10-19 23:15:26 +0000550def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000551 """Prints statistics about the change to the user."""
552 # --no-ext-diff is broken in some versions of Git, so try to work around
553 # this by overriding the environment (but there is still a problem if the
554 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000555 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000556 if 'GIT_EXTERNAL_DIFF' in env:
557 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000558
559 if find_copies:
560 similarity_options = ['--find-copies-harder', '-l100000',
561 '-C%s' % similarity]
562 else:
563 similarity_options = ['-M%s' % similarity]
564
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000565 try:
566 stdout = sys.stdout.fileno()
567 except AttributeError:
568 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000569 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000570 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000571 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000572 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000573
574
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000575class BuildbucketResponseException(Exception):
576 pass
577
578
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000579class Settings(object):
580 def __init__(self):
581 self.default_server = None
582 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000583 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000584 self.is_git_svn = None
585 self.svn_branch = None
586 self.tree_status_url = None
587 self.viewvc_url = None
588 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000589 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000590 self.squash_gerrit_uploads = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000591 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000592 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000593 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000594 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000595
596 def LazyUpdateIfNeeded(self):
597 """Updates the settings from a codereview.settings file, if available."""
598 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000599 # The only value that actually changes the behavior is
600 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000601 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000602 error_ok=True
603 ).strip().lower()
604
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000605 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000606 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000607 LoadCodereviewSettingsFromFile(cr_settings_file)
608 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
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000633 def GetGitMirror(self, remote='origin'):
634 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000635 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000636 if not os.path.isdir(local_url):
637 return None
638 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
639 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
640 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
641 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
642 if mirror.exists():
643 return mirror
644 return None
645
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000646 def GetIsGitSvn(self):
647 """Return true if this repo looks like it's using git-svn."""
648 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000649 if self.GetPendingRefPrefix():
650 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
651 self.is_git_svn = False
652 else:
653 # If you have any "svn-remote.*" config keys, we think you're using svn.
654 self.is_git_svn = RunGitWithCode(
655 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000656 return self.is_git_svn
657
658 def GetSVNBranch(self):
659 if self.svn_branch is None:
660 if not self.GetIsGitSvn():
661 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
662
663 # Try to figure out which remote branch we're based on.
664 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000665 # 1) iterate through our branch history and find the svn URL.
666 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000667
668 # regexp matching the git-svn line that contains the URL.
669 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
670
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000671 # We don't want to go through all of history, so read a line from the
672 # pipe at a time.
673 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000674 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000675 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
676 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000677 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000678 for line in proc.stdout:
679 match = git_svn_re.match(line)
680 if match:
681 url = match.group(1)
682 proc.stdout.close() # Cut pipe.
683 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000684
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000685 if url:
686 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
687 remotes = RunGit(['config', '--get-regexp',
688 r'^svn-remote\..*\.url']).splitlines()
689 for remote in remotes:
690 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000691 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000692 remote = match.group(1)
693 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000694 rewrite_root = RunGit(
695 ['config', 'svn-remote.%s.rewriteRoot' % remote],
696 error_ok=True).strip()
697 if rewrite_root:
698 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000699 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000700 ['config', 'svn-remote.%s.fetch' % remote],
701 error_ok=True).strip()
702 if fetch_spec:
703 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
704 if self.svn_branch:
705 break
706 branch_spec = RunGit(
707 ['config', 'svn-remote.%s.branches' % remote],
708 error_ok=True).strip()
709 if branch_spec:
710 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
711 if self.svn_branch:
712 break
713 tag_spec = RunGit(
714 ['config', 'svn-remote.%s.tags' % remote],
715 error_ok=True).strip()
716 if tag_spec:
717 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
718 if self.svn_branch:
719 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000720
721 if not self.svn_branch:
722 DieWithError('Can\'t guess svn branch -- try specifying it on the '
723 'command line')
724
725 return self.svn_branch
726
727 def GetTreeStatusUrl(self, error_ok=False):
728 if not self.tree_status_url:
729 error_message = ('You must configure your tree status URL by running '
730 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000731 self.tree_status_url = self._GetRietveldConfig(
732 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000733 return self.tree_status_url
734
735 def GetViewVCUrl(self):
736 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000737 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000738 return self.viewvc_url
739
rmistry@google.com90752582014-01-14 21:04:50 +0000740 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000741 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000742
rmistry@google.com78948ed2015-07-08 23:09:57 +0000743 def GetIsSkipDependencyUpload(self, branch_name):
744 """Returns true if specified branch should skip dep uploads."""
745 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
746 error_ok=True)
747
rmistry@google.com5626a922015-02-26 14:03:30 +0000748 def GetRunPostUploadHook(self):
749 run_post_upload_hook = self._GetRietveldConfig(
750 'run-post-upload-hook', error_ok=True)
751 return run_post_upload_hook == "True"
752
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000753 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000754 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000755
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000756 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000757 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000758
ukai@chromium.orge8077812012-02-03 03:41:46 +0000759 def GetIsGerrit(self):
760 """Return true if this repo is assosiated with gerrit code review system."""
761 if self.is_gerrit is None:
762 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
763 return self.is_gerrit
764
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000765 def GetSquashGerritUploads(self):
766 """Return true if uploads to Gerrit should be squashed by default."""
767 if self.squash_gerrit_uploads is None:
768 self.squash_gerrit_uploads = (
769 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
770 error_ok=True).strip() == 'true')
771 return self.squash_gerrit_uploads
772
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000773 def GetGitEditor(self):
774 """Return the editor specified in the git config, or None if none is."""
775 if self.git_editor is None:
776 self.git_editor = self._GetConfig('core.editor', error_ok=True)
777 return self.git_editor or None
778
thestig@chromium.org44202a22014-03-11 19:22:18 +0000779 def GetLintRegex(self):
780 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
781 DEFAULT_LINT_REGEX)
782
783 def GetLintIgnoreRegex(self):
784 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
785 DEFAULT_LINT_IGNORE_REGEX)
786
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000787 def GetProject(self):
788 if not self.project:
789 self.project = self._GetRietveldConfig('project', error_ok=True)
790 return self.project
791
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000792 def GetForceHttpsCommitUrl(self):
793 if not self.force_https_commit_url:
794 self.force_https_commit_url = self._GetRietveldConfig(
795 'force-https-commit-url', error_ok=True)
796 return self.force_https_commit_url
797
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000798 def GetPendingRefPrefix(self):
799 if not self.pending_ref_prefix:
800 self.pending_ref_prefix = self._GetRietveldConfig(
801 'pending-ref-prefix', error_ok=True)
802 return self.pending_ref_prefix
803
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000804 def _GetRietveldConfig(self, param, **kwargs):
805 return self._GetConfig('rietveld.' + param, **kwargs)
806
rmistry@google.com78948ed2015-07-08 23:09:57 +0000807 def _GetBranchConfig(self, branch_name, param, **kwargs):
808 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
809
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000810 def _GetConfig(self, param, **kwargs):
811 self.LazyUpdateIfNeeded()
812 return RunGit(['config', param], **kwargs).strip()
813
814
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000815def ShortBranchName(branch):
816 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000817 return branch.replace('refs/heads/', '', 1)
818
819
820def GetCurrentBranchRef():
821 """Returns branch ref (e.g., refs/heads/master) or None."""
822 return RunGit(['symbolic-ref', 'HEAD'],
823 stderr=subprocess2.VOID, error_ok=True).strip() or None
824
825
826def GetCurrentBranch():
827 """Returns current branch or None.
828
829 For refs/heads/* branches, returns just last part. For others, full ref.
830 """
831 branchref = GetCurrentBranchRef()
832 if branchref:
833 return ShortBranchName(branchref)
834 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000835
836
837class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000838 """Changelist works with one changelist in local branch.
839
840 Supports two codereview backends: Rietveld or Gerrit, selected at object
841 creation.
842
843 Not safe for concurrent multi-{thread,process} use.
844 """
845
846 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
847 """Create a new ChangeList instance.
848
849 If issue is given, the codereview must be given too.
850
851 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
852 Otherwise, it's decided based on current configuration of the local branch,
853 with default being 'rietveld' for backwards compatibility.
854 See _load_codereview_impl for more details.
855
856 **kwargs will be passed directly to codereview implementation.
857 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000858 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000859 global settings
860 if not settings:
861 # Happens when git_cl.py is used as a utility library.
862 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000863
864 if issue:
865 assert codereview, 'codereview must be known, if issue is known'
866
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000867 self.branchref = branchref
868 if self.branchref:
869 self.branch = ShortBranchName(self.branchref)
870 else:
871 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000872 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000873 self.lookedup_issue = False
874 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000875 self.has_description = False
876 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000877 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000878 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000879 self.cc = None
880 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000881 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000882
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000883 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000884 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000885 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000886 assert self._codereview_impl
887 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000888
889 def _load_codereview_impl(self, codereview=None, **kwargs):
890 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000891 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
892 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
893 self._codereview = codereview
894 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000895 return
896
897 # Automatic selection based on issue number set for a current branch.
898 # Rietveld takes precedence over Gerrit.
899 assert not self.issue
900 # Whether we find issue or not, we are doing the lookup.
901 self.lookedup_issue = True
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000902 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000903 setting = cls.IssueSetting(self.GetBranch())
904 issue = RunGit(['config', setting], error_ok=True).strip()
905 if issue:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000906 self._codereview = codereview
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000907 self._codereview_impl = cls(self, **kwargs)
908 self.issue = int(issue)
909 return
910
911 # No issue is set for this branch, so decide based on repo-wide settings.
912 return self._load_codereview_impl(
913 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
914 **kwargs)
915
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000916 def IsGerrit(self):
917 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000918
919 def GetCCList(self):
920 """Return the users cc'd on this CL.
921
922 Return is a string suitable for passing to gcl with the --cc flag.
923 """
924 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000925 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000926 more_cc = ','.join(self.watchers)
927 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
928 return self.cc
929
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000930 def GetCCListWithoutDefault(self):
931 """Return the users cc'd on this CL excluding default ones."""
932 if self.cc is None:
933 self.cc = ','.join(self.watchers)
934 return self.cc
935
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000936 def SetWatchers(self, watchers):
937 """Set the list of email addresses that should be cc'd based on the changed
938 files in this CL.
939 """
940 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000941
942 def GetBranch(self):
943 """Returns the short branch name, e.g. 'master'."""
944 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000945 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000946 if not branchref:
947 return None
948 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000949 self.branch = ShortBranchName(self.branchref)
950 return self.branch
951
952 def GetBranchRef(self):
953 """Returns the full branch name, e.g. 'refs/heads/master'."""
954 self.GetBranch() # Poke the lazy loader.
955 return self.branchref
956
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000957 @staticmethod
958 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000959 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000960 e.g. 'origin', 'refs/heads/master'
961 """
962 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000963 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
964 error_ok=True).strip()
965 if upstream_branch:
966 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
967 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000968 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
969 error_ok=True).strip()
970 if upstream_branch:
971 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000972 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000973 # Fall back on trying a git-svn upstream branch.
974 if settings.GetIsGitSvn():
975 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000976 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000977 # Else, try to guess the origin remote.
978 remote_branches = RunGit(['branch', '-r']).split()
979 if 'origin/master' in remote_branches:
980 # Fall back on origin/master if it exits.
981 remote = 'origin'
982 upstream_branch = 'refs/heads/master'
983 elif 'origin/trunk' in remote_branches:
984 # Fall back on origin/trunk if it exists. Generally a shared
985 # git-svn clone
986 remote = 'origin'
987 upstream_branch = 'refs/heads/trunk'
988 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000989 DieWithError(
990 'Unable to determine default branch to diff against.\n'
991 'Either pass complete "git diff"-style arguments, like\n'
992 ' git cl upload origin/master\n'
993 'or verify this branch is set up to track another \n'
994 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000995
996 return remote, upstream_branch
997
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000998 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000999 upstream_branch = self.GetUpstreamBranch()
1000 if not BranchExists(upstream_branch):
1001 DieWithError('The upstream for the current branch (%s) does not exist '
1002 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001003 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001004 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001005
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001006 def GetUpstreamBranch(self):
1007 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001008 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001009 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001010 upstream_branch = upstream_branch.replace('refs/heads/',
1011 'refs/remotes/%s/' % remote)
1012 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1013 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001014 self.upstream_branch = upstream_branch
1015 return self.upstream_branch
1016
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001017 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001018 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001019 remote, branch = None, self.GetBranch()
1020 seen_branches = set()
1021 while branch not in seen_branches:
1022 seen_branches.add(branch)
1023 remote, branch = self.FetchUpstreamTuple(branch)
1024 branch = ShortBranchName(branch)
1025 if remote != '.' or branch.startswith('refs/remotes'):
1026 break
1027 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001028 remotes = RunGit(['remote'], error_ok=True).split()
1029 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001030 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001031 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001032 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001033 logging.warning('Could not determine which remote this change is '
1034 'associated with, so defaulting to "%s". This may '
1035 'not be what you want. You may prevent this message '
1036 'by running "git svn info" as documented here: %s',
1037 self._remote,
1038 GIT_INSTRUCTIONS_URL)
1039 else:
1040 logging.warn('Could not determine which remote this change is '
1041 'associated with. You may prevent this message by '
1042 'running "git svn info" as documented here: %s',
1043 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001044 branch = 'HEAD'
1045 if branch.startswith('refs/remotes'):
1046 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001047 elif branch.startswith('refs/branch-heads/'):
1048 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001049 else:
1050 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001051 return self._remote
1052
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001053 def GitSanityChecks(self, upstream_git_obj):
1054 """Checks git repo status and ensures diff is from local commits."""
1055
sbc@chromium.org79706062015-01-14 21:18:12 +00001056 if upstream_git_obj is None:
1057 if self.GetBranch() is None:
1058 print >> sys.stderr, (
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00001059 'ERROR: unable to determine current branch (detached HEAD?)')
sbc@chromium.org79706062015-01-14 21:18:12 +00001060 else:
1061 print >> sys.stderr, (
1062 'ERROR: no upstream branch')
1063 return False
1064
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001065 # Verify the commit we're diffing against is in our current branch.
1066 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1067 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1068 if upstream_sha != common_ancestor:
1069 print >> sys.stderr, (
1070 'ERROR: %s is not in the current branch. You may need to rebase '
1071 'your tracking branch' % upstream_sha)
1072 return False
1073
1074 # List the commits inside the diff, and verify they are all local.
1075 commits_in_diff = RunGit(
1076 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1077 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1078 remote_branch = remote_branch.strip()
1079 if code != 0:
1080 _, remote_branch = self.GetRemoteBranch()
1081
1082 commits_in_remote = RunGit(
1083 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1084
1085 common_commits = set(commits_in_diff) & set(commits_in_remote)
1086 if common_commits:
1087 print >> sys.stderr, (
1088 'ERROR: Your diff contains %d commits already in %s.\n'
1089 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1090 'the diff. If you are using a custom git flow, you can override'
1091 ' the reference used for this check with "git config '
1092 'gitcl.remotebranch <git-ref>".' % (
1093 len(common_commits), remote_branch, upstream_git_obj))
1094 return False
1095 return True
1096
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001097 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001098 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001099
1100 Returns None if it is not set.
1101 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001102 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
1103 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001104
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001105 def GetGitSvnRemoteUrl(self):
1106 """Return the configured git-svn remote URL parsed from git svn info.
1107
1108 Returns None if it is not set.
1109 """
1110 # URL is dependent on the current directory.
1111 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1112 if data:
1113 keys = dict(line.split(': ', 1) for line in data.splitlines()
1114 if ': ' in line)
1115 return keys.get('URL', None)
1116 return None
1117
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001118 def GetRemoteUrl(self):
1119 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1120
1121 Returns None if there is no remote.
1122 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001123 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001124 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1125
1126 # If URL is pointing to a local directory, it is probably a git cache.
1127 if os.path.isdir(url):
1128 url = RunGit(['config', 'remote.%s.url' % remote],
1129 error_ok=True,
1130 cwd=url).strip()
1131 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001132
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001133 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001134 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001135 if self.issue is None and not self.lookedup_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001136 issue = RunGit(['config',
1137 self._codereview_impl.IssueSetting(self.GetBranch())],
1138 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001139 self.issue = int(issue) or None if issue else None
1140 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001141 return self.issue
1142
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001143 def GetIssueURL(self):
1144 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001145 issue = self.GetIssue()
1146 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001147 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001148 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001149
1150 def GetDescription(self, pretty=False):
1151 if not self.has_description:
1152 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001153 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001154 self.has_description = True
1155 if pretty:
1156 wrapper = textwrap.TextWrapper()
1157 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1158 return wrapper.fill(self.description)
1159 return self.description
1160
1161 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001162 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001163 if self.patchset is None and not self.lookedup_patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001164 patchset = RunGit(['config', self._codereview_impl.PatchsetSetting()],
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001165 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001166 self.patchset = int(patchset) or None if patchset else None
1167 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001168 return self.patchset
1169
1170 def SetPatchset(self, patchset):
1171 """Set this branch's patchset. If patchset=0, clears the patchset."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001172 patchset_setting = self._codereview_impl.PatchsetSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001173 if patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001174 RunGit(['config', patchset_setting, str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001175 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001176 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001177 RunGit(['config', '--unset', patchset_setting],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001178 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001179 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001180
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001181 def SetIssue(self, issue=None):
1182 """Set this branch's issue. If issue isn't given, clears the issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001183 issue_setting = self._codereview_impl.IssueSetting(self.GetBranch())
1184 codereview_setting = self._codereview_impl.GetCodereviewServerSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001185 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001186 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001187 RunGit(['config', issue_setting, str(issue)])
1188 codereview_server = self._codereview_impl.GetCodereviewServer()
1189 if codereview_server:
1190 RunGit(['config', codereview_setting, codereview_server])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001191 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +00001192 current_issue = self.GetIssue()
1193 if current_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001194 RunGit(['config', '--unset', issue_setting])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001195 self.issue = None
1196 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001197
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001198 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001199 if not self.GitSanityChecks(upstream_branch):
1200 DieWithError('\nGit sanity check failure')
1201
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001202 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001203 if not root:
1204 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001205 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001206
1207 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001208 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001209 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001210 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001211 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001212 except subprocess2.CalledProcessError:
1213 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001214 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001215 'This branch probably doesn\'t exist anymore. To reset the\n'
1216 'tracking branch, please run\n'
1217 ' git branch --set-upstream %s trunk\n'
1218 'replacing trunk with origin/master or the relevant branch') %
1219 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001220
maruel@chromium.org52424302012-08-29 15:14:30 +00001221 issue = self.GetIssue()
1222 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001223 if issue:
1224 description = self.GetDescription()
1225 else:
1226 # If the change was never uploaded, use the log messages of all commits
1227 # up to the branch point, as git cl upload will prefill the description
1228 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001229 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1230 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001231
1232 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001233 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001234 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001235 name,
1236 description,
1237 absroot,
1238 files,
1239 issue,
1240 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001241 author,
1242 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001243
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001244 def UpdateDescription(self, description):
1245 self.description = description
1246 return self._codereview_impl.UpdateDescriptionRemote(description)
1247
1248 def RunHook(self, committing, may_prompt, verbose, change):
1249 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1250 try:
1251 return presubmit_support.DoPresubmitChecks(change, committing,
1252 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1253 default_presubmit=None, may_prompt=may_prompt,
1254 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit())
1255 except presubmit_support.PresubmitFailure, e:
1256 DieWithError(
1257 ('%s\nMaybe your depot_tools is out of date?\n'
1258 'If all fails, contact maruel@') % e)
1259
1260 # Forward methods to codereview specific implementation.
1261
1262 def CloseIssue(self):
1263 return self._codereview_impl.CloseIssue()
1264
1265 def GetStatus(self):
1266 return self._codereview_impl.GetStatus()
1267
1268 def GetCodereviewServer(self):
1269 return self._codereview_impl.GetCodereviewServer()
1270
1271 def GetApprovingReviewers(self):
1272 return self._codereview_impl.GetApprovingReviewers()
1273
1274 def GetMostRecentPatchset(self):
1275 return self._codereview_impl.GetMostRecentPatchset()
1276
1277 def __getattr__(self, attr):
1278 # This is because lots of untested code accesses Rietveld-specific stuff
1279 # directly, and it's hard to fix for sure. So, just let it work, and fix
1280 # on a cases by case basis.
1281 return getattr(self._codereview_impl, attr)
1282
1283
1284class _ChangelistCodereviewBase(object):
1285 """Abstract base class encapsulating codereview specifics of a changelist."""
1286 def __init__(self, changelist):
1287 self._changelist = changelist # instance of Changelist
1288
1289 def __getattr__(self, attr):
1290 # Forward methods to changelist.
1291 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1292 # _RietveldChangelistImpl to avoid this hack?
1293 return getattr(self._changelist, attr)
1294
1295 def GetStatus(self):
1296 """Apply a rough heuristic to give a simple summary of an issue's review
1297 or CQ status, assuming adherence to a common workflow.
1298
1299 Returns None if no issue for this branch, or specific string keywords.
1300 """
1301 raise NotImplementedError()
1302
1303 def GetCodereviewServer(self):
1304 """Returns server URL without end slash, like "https://codereview.com"."""
1305 raise NotImplementedError()
1306
1307 def FetchDescription(self):
1308 """Fetches and returns description from the codereview server."""
1309 raise NotImplementedError()
1310
1311 def GetCodereviewServerSetting(self):
1312 """Returns git config setting for the codereview server."""
1313 raise NotImplementedError()
1314
1315 @staticmethod
1316 def IssueSetting(branch):
1317 """Returns name of git config setting which stores issue number for a given
1318 branch."""
1319 raise NotImplementedError()
1320
1321 def PatchsetSetting(self):
1322 """Returns name of git config setting which stores issue number."""
1323 raise NotImplementedError()
1324
1325 def GetRieveldObjForPresubmit(self):
1326 # This is an unfortunate Rietveld-embeddedness in presubmit.
1327 # For non-Rietveld codereviews, this probably should return a dummy object.
1328 raise NotImplementedError()
1329
1330 def UpdateDescriptionRemote(self, description):
1331 """Update the description on codereview site."""
1332 raise NotImplementedError()
1333
1334 def CloseIssue(self):
1335 """Closes the issue."""
1336 raise NotImplementedError()
1337
1338 def GetApprovingReviewers(self):
1339 """Returns a list of reviewers approving the change.
1340
1341 Note: not necessarily committers.
1342 """
1343 raise NotImplementedError()
1344
1345 def GetMostRecentPatchset(self):
1346 """Returns the most recent patchset number from the codereview site."""
1347 raise NotImplementedError()
1348
1349
1350class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1351 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1352 super(_RietveldChangelistImpl, self).__init__(changelist)
1353 assert settings, 'must be initialized in _ChangelistCodereviewBase'
1354 settings.GetDefaultServerUrl()
1355
1356 self._rietveld_server = rietveld_server
1357 self._auth_config = auth_config
1358 self._props = None
1359 self._rpc_server = None
1360
1361 def GetAuthConfig(self):
1362 return self._auth_config
1363
1364 def GetCodereviewServer(self):
1365 if not self._rietveld_server:
1366 # If we're on a branch then get the server potentially associated
1367 # with that branch.
1368 if self.GetIssue():
1369 rietveld_server_setting = self.GetCodereviewServerSetting()
1370 if rietveld_server_setting:
1371 self._rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1372 ['config', rietveld_server_setting], error_ok=True).strip())
1373 if not self._rietveld_server:
1374 self._rietveld_server = settings.GetDefaultServerUrl()
1375 return self._rietveld_server
1376
1377 def FetchDescription(self):
1378 issue = self.GetIssue()
1379 assert issue
1380 try:
1381 return self.RpcServer().get_description(issue).strip()
1382 except urllib2.HTTPError as e:
1383 if e.code == 404:
1384 DieWithError(
1385 ('\nWhile fetching the description for issue %d, received a '
1386 '404 (not found)\n'
1387 'error. It is likely that you deleted this '
1388 'issue on the server. If this is the\n'
1389 'case, please run\n\n'
1390 ' git cl issue 0\n\n'
1391 'to clear the association with the deleted issue. Then run '
1392 'this command again.') % issue)
1393 else:
1394 DieWithError(
1395 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1396 except urllib2.URLError as e:
1397 print >> sys.stderr, (
1398 'Warning: Failed to retrieve CL description due to network '
1399 'failure.')
1400 return ''
1401
1402 def GetMostRecentPatchset(self):
1403 return self.GetIssueProperties()['patchsets'][-1]
1404
1405 def GetPatchSetDiff(self, issue, patchset):
1406 return self.RpcServer().get(
1407 '/download/issue%s_%s.diff' % (issue, patchset))
1408
1409 def GetIssueProperties(self):
1410 if self._props is None:
1411 issue = self.GetIssue()
1412 if not issue:
1413 self._props = {}
1414 else:
1415 self._props = self.RpcServer().get_issue_properties(issue, True)
1416 return self._props
1417
1418 def GetApprovingReviewers(self):
1419 return get_approving_reviewers(self.GetIssueProperties())
1420
1421 def AddComment(self, message):
1422 return self.RpcServer().add_comment(self.GetIssue(), message)
1423
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001424 def GetStatus(self):
1425 """Apply a rough heuristic to give a simple summary of an issue's review
1426 or CQ status, assuming adherence to a common workflow.
1427
1428 Returns None if no issue for this branch, or one of the following keywords:
1429 * 'error' - error from review tool (including deleted issues)
1430 * 'unsent' - not sent for review
1431 * 'waiting' - waiting for review
1432 * 'reply' - waiting for owner to reply to review
1433 * 'lgtm' - LGTM from at least one approved reviewer
1434 * 'commit' - in the commit queue
1435 * 'closed' - closed
1436 """
1437 if not self.GetIssue():
1438 return None
1439
1440 try:
1441 props = self.GetIssueProperties()
1442 except urllib2.HTTPError:
1443 return 'error'
1444
1445 if props.get('closed'):
1446 # Issue is closed.
1447 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001448 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001449 # Issue is in the commit queue.
1450 return 'commit'
1451
1452 try:
1453 reviewers = self.GetApprovingReviewers()
1454 except urllib2.HTTPError:
1455 return 'error'
1456
1457 if reviewers:
1458 # Was LGTM'ed.
1459 return 'lgtm'
1460
1461 messages = props.get('messages') or []
1462
1463 if not messages:
1464 # No message was sent.
1465 return 'unsent'
1466 if messages[-1]['sender'] != props.get('owner_email'):
1467 # Non-LGTM reply from non-owner
1468 return 'reply'
1469 return 'waiting'
1470
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001471 def UpdateDescriptionRemote(self, description):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001472 return self.RpcServer().update_description(
1473 self.GetIssue(), self.description)
1474
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001475 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001476 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001477
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001478 def SetFlag(self, flag, value):
1479 """Patchset must match."""
1480 if not self.GetPatchset():
1481 DieWithError('The patchset needs to match. Send another patchset.')
1482 try:
1483 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001484 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001485 except urllib2.HTTPError, e:
1486 if e.code == 404:
1487 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1488 if e.code == 403:
1489 DieWithError(
1490 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1491 'match?') % (self.GetIssue(), self.GetPatchset()))
1492 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001493
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001494 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001495 """Returns an upload.RpcServer() to access this review's rietveld instance.
1496 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001497 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001498 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001499 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001500 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001501 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001502
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001503 @staticmethod
1504 def IssueSetting(branch):
1505 return 'branch.%s.rietveldissue' % branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001506
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001507 def PatchsetSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001508 """Return the git setting that stores this change's most recent patchset."""
1509 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1510
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001511 def GetCodereviewServerSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001512 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001513 branch = self.GetBranch()
1514 if branch:
1515 return 'branch.%s.rietveldserver' % branch
1516 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001517
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001518 def GetRieveldObjForPresubmit(self):
1519 return self.RpcServer()
1520
1521
1522class _GerritChangelistImpl(_ChangelistCodereviewBase):
1523 def __init__(self, changelist, auth_config=None):
1524 # auth_config is Rietveld thing, kept here to preserve interface only.
1525 super(_GerritChangelistImpl, self).__init__(changelist)
1526 self._change_id = None
1527 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
1528 self._gerrit_host = None # e.g. chromium-review.googlesource.com
1529
1530 def _GetGerritHost(self):
1531 # Lazy load of configs.
1532 self.GetCodereviewServer()
1533 return self._gerrit_host
1534
1535 def GetCodereviewServer(self):
1536 if not self._gerrit_server:
1537 # If we're on a branch then get the server potentially associated
1538 # with that branch.
1539 if self.GetIssue():
1540 gerrit_server_setting = self.GetCodereviewServerSetting()
1541 if gerrit_server_setting:
1542 self._gerrit_server = RunGit(['config', gerrit_server_setting],
1543 error_ok=True).strip()
1544 if self._gerrit_server:
1545 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
1546 if not self._gerrit_server:
1547 # We assume repo to be hosted on Gerrit, and hence Gerrit server
1548 # has "-review" suffix for lowest level subdomain.
1549 parts = urlparse.urlparse(self.GetRemoteUrl()).netloc.split('.')
1550 parts[0] = parts[0] + '-review'
1551 self._gerrit_host = '.'.join(parts)
1552 self._gerrit_server = 'https://%s' % self._gerrit_host
1553 return self._gerrit_server
1554
1555 @staticmethod
1556 def IssueSetting(branch):
1557 return 'branch.%s.gerritissue' % branch
1558
1559 def PatchsetSetting(self):
1560 """Return the git setting that stores this change's most recent patchset."""
1561 return 'branch.%s.gerritpatchset' % self.GetBranch()
1562
1563 def GetCodereviewServerSetting(self):
1564 """Returns the git setting that stores this change's Gerrit server."""
1565 branch = self.GetBranch()
1566 if branch:
1567 return 'branch.%s.gerritserver' % branch
1568 return None
1569
1570 def GetRieveldObjForPresubmit(self):
1571 class ThisIsNotRietveldIssue(object):
1572 def __nonzero__(self):
1573 # This is a hack to make presubmit_support think that rietveld is not
1574 # defined, yet still ensure that calls directly result in a decent
1575 # exception message below.
1576 return False
1577
1578 def __getattr__(self, attr):
1579 print(
1580 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
1581 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
1582 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
1583 'or use Rietveld for codereview.\n'
1584 'See also http://crbug.com/579160.' % attr)
1585 raise NotImplementedError()
1586 return ThisIsNotRietveldIssue()
1587
1588 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001589 """Apply a rough heuristic to give a simple summary of an issue's review
1590 or CQ status, assuming adherence to a common workflow.
1591
1592 Returns None if no issue for this branch, or one of the following keywords:
1593 * 'error' - error from review tool (including deleted issues)
1594 * 'unsent' - no reviewers added
1595 * 'waiting' - waiting for review
1596 * 'reply' - waiting for owner to reply to review
1597 * 'not lgtm' - Code-Review -2 from at least one approved reviewer
1598 * 'lgtm' - Code-Review +2 from at least one approved reviewer
1599 * 'commit' - in the commit queue
1600 * 'closed' - abandoned
1601 """
1602 if not self.GetIssue():
1603 return None
1604
1605 try:
1606 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
1607 except httplib.HTTPException:
1608 return 'error'
1609
1610 if data['status'] == 'ABANDONED':
1611 return 'closed'
1612
1613 cq_label = data['labels'].get('Commit-Queue', {})
1614 if cq_label:
1615 # Vote value is a stringified integer, which we expect from 0 to 2.
1616 vote_value = cq_label.get('value', '0')
1617 vote_text = cq_label.get('values', {}).get(vote_value, '')
1618 if vote_text.lower() == 'commit':
1619 return 'commit'
1620
1621 lgtm_label = data['labels'].get('Code-Review', {})
1622 if lgtm_label:
1623 if 'rejected' in lgtm_label:
1624 return 'not lgtm'
1625 if 'approved' in lgtm_label:
1626 return 'lgtm'
1627
1628 if not data.get('reviewers', {}).get('REVIEWER', []):
1629 return 'unsent'
1630
1631 messages = data.get('messages', [])
1632 if messages:
1633 owner = data['owner'].get('_account_id')
1634 last_message_author = messages[-1].get('author', {}).get('_account_id')
1635 if owner != last_message_author:
1636 # Some reply from non-owner.
1637 return 'reply'
1638
1639 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001640
1641 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001642 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001643 return data['revisions'][data['current_revision']]['_number']
1644
1645 def FetchDescription(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001646 data = self._GetChangeDetail(['COMMIT_FOOTERS', 'CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001647 return data['revisions'][data['current_revision']]['commit_with_footers']
1648
1649 def UpdateDescriptionRemote(self, description):
1650 # TODO(tandrii)
1651 raise NotImplementedError()
1652
1653 def CloseIssue(self):
1654 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
1655
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001656 def SubmitIssue(self, wait_for_merge=True):
1657 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
1658 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001659
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001660 def _GetChangeDetail(self, options):
1661 return gerrit_util.GetChangeDetail(self._GetGerritHost(), self.GetIssue(),
1662 options)
1663
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001664 def CMDLand(self, force, bypass_hooks, verbose):
1665 if git_common.is_dirty_git_tree('land'):
1666 return 1
1667 differs = True
1668 last_upload = RunGit(['config',
1669 'branch.%s.gerritsquashhash' % self.GetBranch()],
1670 error_ok=True).strip()
1671 # Note: git diff outputs nothing if there is no diff.
1672 if not last_upload or RunGit(['diff', last_upload]).strip():
1673 print('WARNING: some changes from local branch haven\'t been uploaded')
1674 else:
1675 detail = self._GetChangeDetail(['CURRENT_REVISION'])
1676 if detail['current_revision'] == last_upload:
1677 differs = False
1678 else:
1679 print('WARNING: local branch contents differ from latest uploaded '
1680 'patchset')
1681 if differs:
1682 if not force:
1683 ask_for_data(
1684 'Do you want to submit latest Gerrit patchset and bypass hooks?')
1685 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
1686 elif not bypass_hooks:
1687 hook_results = self.RunHook(
1688 committing=True,
1689 may_prompt=not force,
1690 verbose=verbose,
1691 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
1692 if not hook_results.should_continue():
1693 return 1
1694
1695 self.SubmitIssue(wait_for_merge=True)
1696 print('Issue %s has been submitted.' % self.GetIssueURL())
1697 return 0
1698
1699
1700_CODEREVIEW_IMPLEMENTATIONS = {
1701 'rietveld': _RietveldChangelistImpl,
1702 'gerrit': _GerritChangelistImpl,
1703}
1704
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001705
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001706class ChangeDescription(object):
1707 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001708 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00001709 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001710
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001711 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00001712 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001713
agable@chromium.org42c20792013-09-12 17:34:49 +00001714 @property # www.logilab.org/ticket/89786
1715 def description(self): # pylint: disable=E0202
1716 return '\n'.join(self._description_lines)
1717
1718 def set_description(self, desc):
1719 if isinstance(desc, basestring):
1720 lines = desc.splitlines()
1721 else:
1722 lines = [line.rstrip() for line in desc]
1723 while lines and not lines[0]:
1724 lines.pop(0)
1725 while lines and not lines[-1]:
1726 lines.pop(-1)
1727 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001728
piman@chromium.org336f9122014-09-04 02:16:55 +00001729 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001730 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001731 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001732 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001733 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001734 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001735
agable@chromium.org42c20792013-09-12 17:34:49 +00001736 # Get the set of R= and TBR= lines and remove them from the desciption.
1737 regexp = re.compile(self.R_LINE)
1738 matches = [regexp.match(line) for line in self._description_lines]
1739 new_desc = [l for i, l in enumerate(self._description_lines)
1740 if not matches[i]]
1741 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001742
agable@chromium.org42c20792013-09-12 17:34:49 +00001743 # Construct new unified R= and TBR= lines.
1744 r_names = []
1745 tbr_names = []
1746 for match in matches:
1747 if not match:
1748 continue
1749 people = cleanup_list([match.group(2).strip()])
1750 if match.group(1) == 'TBR':
1751 tbr_names.extend(people)
1752 else:
1753 r_names.extend(people)
1754 for name in r_names:
1755 if name not in reviewers:
1756 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001757 if add_owners_tbr:
1758 owners_db = owners.Database(change.RepositoryRoot(),
1759 fopen=file, os_path=os.path, glob=glob.glob)
1760 all_reviewers = set(tbr_names + reviewers)
1761 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1762 all_reviewers)
1763 tbr_names.extend(owners_db.reviewers_for(missing_files,
1764 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001765 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1766 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1767
1768 # Put the new lines in the description where the old first R= line was.
1769 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1770 if 0 <= line_loc < len(self._description_lines):
1771 if new_tbr_line:
1772 self._description_lines.insert(line_loc, new_tbr_line)
1773 if new_r_line:
1774 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001775 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001776 if new_r_line:
1777 self.append_footer(new_r_line)
1778 if new_tbr_line:
1779 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001780
1781 def prompt(self):
1782 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001783 self.set_description([
1784 '# Enter a description of the change.',
1785 '# This will be displayed on the codereview site.',
1786 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00001787 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00001788 '--------------------',
1789 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001790
agable@chromium.org42c20792013-09-12 17:34:49 +00001791 regexp = re.compile(self.BUG_LINE)
1792 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00001793 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00001794 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001795 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001796 if not content:
1797 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001798 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001799
1800 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001801 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1802 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001803 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001804 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001805
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001806 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001807 if self._description_lines:
1808 # Add an empty line if either the last line or the new line isn't a tag.
1809 last_line = self._description_lines[-1]
1810 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1811 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1812 self._description_lines.append('')
1813 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001814
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001815 def get_reviewers(self):
1816 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001817 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1818 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001819 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001820
1821
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001822def get_approving_reviewers(props):
1823 """Retrieves the reviewers that approved a CL from the issue properties with
1824 messages.
1825
1826 Note that the list may contain reviewers that are not committer, thus are not
1827 considered by the CQ.
1828 """
1829 return sorted(
1830 set(
1831 message['sender']
1832 for message in props['messages']
1833 if message['approval'] and message['sender'] in props['reviewers']
1834 )
1835 )
1836
1837
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001838def FindCodereviewSettingsFile(filename='codereview.settings'):
1839 """Finds the given file starting in the cwd and going up.
1840
1841 Only looks up to the top of the repository unless an
1842 'inherit-review-settings-ok' file exists in the root of the repository.
1843 """
1844 inherit_ok_file = 'inherit-review-settings-ok'
1845 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001846 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001847 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1848 root = '/'
1849 while True:
1850 if filename in os.listdir(cwd):
1851 if os.path.isfile(os.path.join(cwd, filename)):
1852 return open(os.path.join(cwd, filename))
1853 if cwd == root:
1854 break
1855 cwd = os.path.dirname(cwd)
1856
1857
1858def LoadCodereviewSettingsFromFile(fileobj):
1859 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001860 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001861
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001862 def SetProperty(name, setting, unset_error_ok=False):
1863 fullname = 'rietveld.' + name
1864 if setting in keyvals:
1865 RunGit(['config', fullname, keyvals[setting]])
1866 else:
1867 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1868
1869 SetProperty('server', 'CODE_REVIEW_SERVER')
1870 # Only server setting is required. Other settings can be absent.
1871 # In that case, we ignore errors raised during option deletion attempt.
1872 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001873 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001874 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1875 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001876 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001877 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001878 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
1879 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001880 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001881 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001882 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00001883 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
1884 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001885
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001886 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001887 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001888
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00001889 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
1890 RunGit(['config', 'gerrit.squash-uploads',
1891 keyvals['GERRIT_SQUASH_UPLOADS']])
1892
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001893 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1894 #should be of the form
1895 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1896 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1897 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1898 keyvals['ORIGIN_URL_CONFIG']])
1899
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001900
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001901def urlretrieve(source, destination):
1902 """urllib is broken for SSL connections via a proxy therefore we
1903 can't use urllib.urlretrieve()."""
1904 with open(destination, 'w') as f:
1905 f.write(urllib2.urlopen(source).read())
1906
1907
ukai@chromium.org712d6102013-11-27 00:52:58 +00001908def hasSheBang(fname):
1909 """Checks fname is a #! script."""
1910 with open(fname) as f:
1911 return f.read(2).startswith('#!')
1912
1913
tandrii@chromium.org18630d62016-03-04 12:06:02 +00001914def DownloadGerritHook(force):
1915 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001916
1917 Args:
1918 force: True to update hooks. False to install hooks if not present.
1919 """
1920 if not settings.GetIsGerrit():
1921 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001922 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001923 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1924 if not os.access(dst, os.X_OK):
1925 if os.path.exists(dst):
1926 if not force:
1927 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001928 try:
tandrii@chromium.org18630d62016-03-04 12:06:02 +00001929 print(
1930 'WARNING: installing Gerrit commit-msg hook.\n'
1931 ' This behavior of git cl will soon be disabled.\n'
1932 ' See bug http://crbug.com/579176.')
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001933 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001934 if not hasSheBang(dst):
1935 DieWithError('Not a script: %s\n'
1936 'You need to download from\n%s\n'
1937 'into .git/hooks/commit-msg and '
1938 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001939 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1940 except Exception:
1941 if os.path.exists(dst):
1942 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001943 DieWithError('\nFailed to download hooks.\n'
1944 'You need to download from\n%s\n'
1945 'into .git/hooks/commit-msg and '
1946 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001947
1948
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00001949
1950def GetRietveldCodereviewSettingsInteractively():
1951 """Prompt the user for settings."""
1952 server = settings.GetDefaultServerUrl(error_ok=True)
1953 prompt = 'Rietveld server (host[:port])'
1954 prompt += ' [%s]' % (server or DEFAULT_SERVER)
1955 newserver = ask_for_data(prompt + ':')
1956 if not server and not newserver:
1957 newserver = DEFAULT_SERVER
1958 if newserver:
1959 newserver = gclient_utils.UpgradeToHttps(newserver)
1960 if newserver != server:
1961 RunGit(['config', 'rietveld.server', newserver])
1962
1963 def SetProperty(initial, caption, name, is_url):
1964 prompt = caption
1965 if initial:
1966 prompt += ' ("x" to clear) [%s]' % initial
1967 new_val = ask_for_data(prompt + ':')
1968 if new_val == 'x':
1969 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
1970 elif new_val:
1971 if is_url:
1972 new_val = gclient_utils.UpgradeToHttps(new_val)
1973 if new_val != initial:
1974 RunGit(['config', 'rietveld.' + name, new_val])
1975
1976 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
1977 SetProperty(settings.GetDefaultPrivateFlag(),
1978 'Private flag (rietveld only)', 'private', False)
1979 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
1980 'tree-status-url', False)
1981 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
1982 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
1983 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
1984 'run-post-upload-hook', False)
1985
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001986@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001987def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001988 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001989
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00001990 print('WARNING: git cl config works for Rietveld only.\n'
1991 'For Gerrit, see http://crbug.com/579160.')
1992 # TODO(tandrii): add Gerrit support as part of http://crbug.com/579160.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001993 parser.add_option('--activate-update', action='store_true',
1994 help='activate auto-updating [rietveld] section in '
1995 '.git/config')
1996 parser.add_option('--deactivate-update', action='store_true',
1997 help='deactivate auto-updating [rietveld] section in '
1998 '.git/config')
1999 options, args = parser.parse_args(args)
2000
2001 if options.deactivate_update:
2002 RunGit(['config', 'rietveld.autoupdate', 'false'])
2003 return
2004
2005 if options.activate_update:
2006 RunGit(['config', '--unset', 'rietveld.autoupdate'])
2007 return
2008
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002009 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002010 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002011 return 0
2012
2013 url = args[0]
2014 if not url.endswith('codereview.settings'):
2015 url = os.path.join(url, 'codereview.settings')
2016
2017 # Load code review settings and download hooks (if available).
2018 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
2019 return 0
2020
2021
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002022def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002023 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002024 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
2025 branch = ShortBranchName(branchref)
2026 _, args = parser.parse_args(args)
2027 if not args:
2028 print("Current base-url:")
2029 return RunGit(['config', 'branch.%s.base-url' % branch],
2030 error_ok=False).strip()
2031 else:
2032 print("Setting base-url to %s" % args[0])
2033 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
2034 error_ok=False).strip()
2035
2036
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002037def color_for_status(status):
2038 """Maps a Changelist status to color, for CMDstatus and other tools."""
2039 return {
2040 'unsent': Fore.RED,
2041 'waiting': Fore.BLUE,
2042 'reply': Fore.YELLOW,
2043 'lgtm': Fore.GREEN,
2044 'commit': Fore.MAGENTA,
2045 'closed': Fore.CYAN,
2046 'error': Fore.WHITE,
2047 }.get(status, Fore.WHITE)
2048
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002049def fetch_cl_status(branch, auth_config=None):
2050 """Fetches information for an issue and returns (branch, issue, status)."""
2051 cl = Changelist(branchref=branch, auth_config=auth_config)
2052 url = cl.GetIssueURL()
2053 status = cl.GetStatus()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002054
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002055 if url and (not status or status == 'error'):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002056 # The issue probably doesn't exist anymore.
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002057 url += ' (broken)'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002058
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002059 return (branch, url, status)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002060
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002061def get_cl_statuses(
2062 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002063 """Returns a blocking iterable of (branch, issue, color) for given branches.
2064
2065 If fine_grained is true, this will fetch CL statuses from the server.
2066 Otherwise, simply indicate if there's a matching url for the given branches.
2067
2068 If max_processes is specified, it is used as the maximum number of processes
2069 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
2070 spawned.
2071 """
2072 # Silence upload.py otherwise it becomes unwieldly.
2073 upload.verbosity = 0
2074
2075 if fine_grained:
2076 # Process one branch synchronously to work through authentication, then
2077 # spawn processes to process all the other branches in parallel.
2078 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002079 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
2080 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002081
2082 branches_to_fetch = branches[1:]
2083 pool = ThreadPool(
2084 min(max_processes, len(branches_to_fetch))
2085 if max_processes is not None
2086 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002087 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002088 yield x
2089 else:
2090 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
2091 for b in branches:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002092 cl = Changelist(branchref=b, auth_config=auth_config)
2093 url = cl.GetIssueURL()
2094 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002095
rmistry@google.com2dd99862015-06-22 12:22:18 +00002096
2097def upload_branch_deps(cl, args):
2098 """Uploads CLs of local branches that are dependents of the current branch.
2099
2100 If the local branch dependency tree looks like:
2101 test1 -> test2.1 -> test3.1
2102 -> test3.2
2103 -> test2.2 -> test3.3
2104
2105 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
2106 run on the dependent branches in this order:
2107 test2.1, test3.1, test3.2, test2.2, test3.3
2108
2109 Note: This function does not rebase your local dependent branches. Use it when
2110 you make a change to the parent branch that will not conflict with its
2111 dependent branches, and you would like their dependencies updated in
2112 Rietveld.
2113 """
2114 if git_common.is_dirty_git_tree('upload-branch-deps'):
2115 return 1
2116
2117 root_branch = cl.GetBranch()
2118 if root_branch is None:
2119 DieWithError('Can\'t find dependent branches from detached HEAD state. '
2120 'Get on a branch!')
2121 if not cl.GetIssue() or not cl.GetPatchset():
2122 DieWithError('Current branch does not have an uploaded CL. We cannot set '
2123 'patchset dependencies without an uploaded CL.')
2124
2125 branches = RunGit(['for-each-ref',
2126 '--format=%(refname:short) %(upstream:short)',
2127 'refs/heads'])
2128 if not branches:
2129 print('No local branches found.')
2130 return 0
2131
2132 # Create a dictionary of all local branches to the branches that are dependent
2133 # on it.
2134 tracked_to_dependents = collections.defaultdict(list)
2135 for b in branches.splitlines():
2136 tokens = b.split()
2137 if len(tokens) == 2:
2138 branch_name, tracked = tokens
2139 tracked_to_dependents[tracked].append(branch_name)
2140
2141 print
2142 print 'The dependent local branches of %s are:' % root_branch
2143 dependents = []
2144 def traverse_dependents_preorder(branch, padding=''):
2145 dependents_to_process = tracked_to_dependents.get(branch, [])
2146 padding += ' '
2147 for dependent in dependents_to_process:
2148 print '%s%s' % (padding, dependent)
2149 dependents.append(dependent)
2150 traverse_dependents_preorder(dependent, padding)
2151 traverse_dependents_preorder(root_branch)
2152 print
2153
2154 if not dependents:
2155 print 'There are no dependent local branches for %s' % root_branch
2156 return 0
2157
2158 print ('This command will checkout all dependent branches and run '
2159 '"git cl upload".')
2160 ask_for_data('[Press enter to continue or ctrl-C to quit]')
2161
andybons@chromium.org962f9462016-02-03 20:00:42 +00002162 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00002163 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00002164 args.extend(['-t', 'Updated patchset dependency'])
2165
rmistry@google.com2dd99862015-06-22 12:22:18 +00002166 # Record all dependents that failed to upload.
2167 failures = {}
2168 # Go through all dependents, checkout the branch and upload.
2169 try:
2170 for dependent_branch in dependents:
2171 print
2172 print '--------------------------------------'
2173 print 'Running "git cl upload" from %s:' % dependent_branch
2174 RunGit(['checkout', '-q', dependent_branch])
2175 print
2176 try:
2177 if CMDupload(OptionParser(), args) != 0:
2178 print 'Upload failed for %s!' % dependent_branch
2179 failures[dependent_branch] = 1
2180 except: # pylint: disable=W0702
2181 failures[dependent_branch] = 1
2182 print
2183 finally:
2184 # Swap back to the original root branch.
2185 RunGit(['checkout', '-q', root_branch])
2186
2187 print
2188 print 'Upload complete for dependent branches!'
2189 for dependent_branch in dependents:
2190 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
2191 print ' %s : %s' % (dependent_branch, upload_status)
2192 print
2193
2194 return 0
2195
2196
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002197def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002198 """Show status of changelists.
2199
2200 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00002201 - Red not sent for review or broken
2202 - Blue waiting for review
2203 - Yellow waiting for you to reply to review
2204 - Green LGTM'ed
2205 - Magenta in the commit queue
2206 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002207
2208 Also see 'git cl comments'.
2209 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002210 parser.add_option('--field',
2211 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002212 parser.add_option('-f', '--fast', action='store_true',
2213 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002214 parser.add_option(
2215 '-j', '--maxjobs', action='store', type=int,
2216 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002217
2218 auth.add_auth_options(parser)
2219 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002220 if args:
2221 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002222 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002223
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002224 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002225 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002226 if options.field.startswith('desc'):
2227 print cl.GetDescription()
2228 elif options.field == 'id':
2229 issueid = cl.GetIssue()
2230 if issueid:
2231 print issueid
2232 elif options.field == 'patch':
2233 patchset = cl.GetPatchset()
2234 if patchset:
2235 print patchset
2236 elif options.field == 'url':
2237 url = cl.GetIssueURL()
2238 if url:
2239 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002240 return 0
2241
2242 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
2243 if not branches:
2244 print('No local branch found.')
2245 return 0
2246
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002247 changes = (
2248 Changelist(branchref=b, auth_config=auth_config)
2249 for b in branches.splitlines())
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002250 # TODO(tandrii): refactor to use CLs list instead of branches list.
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00002251 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002252 alignment = max(5, max(len(b) for b in branches))
2253 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002254 output = get_cl_statuses(branches,
2255 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002256 max_processes=options.maxjobs,
2257 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002258
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002259 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002260 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002261 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002262 while branch not in branch_statuses:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002263 b, i, status = output.next()
2264 branch_statuses[b] = (i, status)
2265 issue_url, status = branch_statuses.pop(branch)
2266 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00002267 reset = Fore.RESET
2268 if not sys.stdout.isatty():
2269 color = ''
2270 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002271 status_str = '(%s)' % status if status else ''
2272 print ' %*s : %s%s %s%s' % (
2273 alignment, ShortBranchName(branch), color, issue_url, status_str,
2274 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002275
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002276 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002277 print
2278 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002279 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00002280 if not cl.GetIssue():
2281 print 'No issue assigned.'
2282 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002283 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00002284 if not options.fast:
2285 print 'Issue description:'
2286 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002287 return 0
2288
2289
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002290def colorize_CMDstatus_doc():
2291 """To be called once in main() to add colors to git cl status help."""
2292 colors = [i for i in dir(Fore) if i[0].isupper()]
2293
2294 def colorize_line(line):
2295 for color in colors:
2296 if color in line.upper():
2297 # Extract whitespaces first and the leading '-'.
2298 indent = len(line) - len(line.lstrip(' ')) + 1
2299 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
2300 return line
2301
2302 lines = CMDstatus.__doc__.splitlines()
2303 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
2304
2305
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002306@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002307def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002308 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002309
2310 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002311 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00002312 parser.add_option('-r', '--reverse', action='store_true',
2313 help='Lookup the branch(es) for the specified issues. If '
2314 'no issues are specified, all branches with mapped '
2315 'issues will be listed.')
2316 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002317
dnj@chromium.org406c4402015-03-03 17:22:28 +00002318 if options.reverse:
2319 branches = RunGit(['for-each-ref', 'refs/heads',
2320 '--format=%(refname:short)']).splitlines()
2321
2322 # Reverse issue lookup.
2323 issue_branch_map = {}
2324 for branch in branches:
2325 cl = Changelist(branchref=branch)
2326 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
2327 if not args:
2328 args = sorted(issue_branch_map.iterkeys())
2329 for issue in args:
2330 if not issue:
2331 continue
2332 print 'Branch for issue number %s: %s' % (
2333 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
2334 else:
2335 cl = Changelist()
2336 if len(args) > 0:
2337 try:
2338 issue = int(args[0])
2339 except ValueError:
2340 DieWithError('Pass a number to set the issue or none to list it.\n'
2341 'Maybe you want to run git cl status?')
2342 cl.SetIssue(issue)
2343 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002344 return 0
2345
2346
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002347def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002348 """Shows or posts review comments for any changelist."""
2349 parser.add_option('-a', '--add-comment', dest='comment',
2350 help='comment to add to an issue')
2351 parser.add_option('-i', dest='issue',
2352 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00002353 parser.add_option('-j', '--json-file',
2354 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002355 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002356 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002357 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002358
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002359 issue = None
2360 if options.issue:
2361 try:
2362 issue = int(options.issue)
2363 except ValueError:
2364 DieWithError('A review issue id is expected to be a number')
2365
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002366 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002367
2368 if options.comment:
2369 cl.AddComment(options.comment)
2370 return 0
2371
2372 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00002373 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00002374 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00002375 summary.append({
2376 'date': message['date'],
2377 'lgtm': False,
2378 'message': message['text'],
2379 'not_lgtm': False,
2380 'sender': message['sender'],
2381 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002382 if message['disapproval']:
2383 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00002384 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002385 elif message['approval']:
2386 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00002387 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002388 elif message['sender'] == data['owner_email']:
2389 color = Fore.MAGENTA
2390 else:
2391 color = Fore.BLUE
2392 print '\n%s%s %s%s' % (
2393 color, message['date'].split('.', 1)[0], message['sender'],
2394 Fore.RESET)
2395 if message['text'].strip():
2396 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00002397 if options.json_file:
2398 with open(options.json_file, 'wb') as f:
2399 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002400 return 0
2401
2402
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002403def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002404 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00002405 parser.add_option('-d', '--display', action='store_true',
2406 help='Display the description instead of opening an editor')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002407 auth.add_auth_options(parser)
2408 options, _ = parser.parse_args(args)
2409 auth_config = auth.extract_auth_config_from_options(options)
2410 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002411 if not cl.GetIssue():
2412 DieWithError('This branch has no associated changelist.')
2413 description = ChangeDescription(cl.GetDescription())
smut@google.com34fb6b12015-07-13 20:03:26 +00002414 if options.display:
2415 print description.description
2416 return 0
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002417 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00002418 if cl.GetDescription() != description.description:
2419 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002420 return 0
2421
2422
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002423def CreateDescriptionFromLog(args):
2424 """Pulls out the commit log to use as a base for the CL description."""
2425 log_args = []
2426 if len(args) == 1 and not args[0].endswith('.'):
2427 log_args = [args[0] + '..']
2428 elif len(args) == 1 and args[0].endswith('...'):
2429 log_args = [args[0][:-1]]
2430 elif len(args) == 2:
2431 log_args = [args[0] + '..' + args[1]]
2432 else:
2433 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00002434 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002435
2436
thestig@chromium.org44202a22014-03-11 19:22:18 +00002437def CMDlint(parser, args):
2438 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002439 parser.add_option('--filter', action='append', metavar='-x,+y',
2440 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002441 auth.add_auth_options(parser)
2442 options, args = parser.parse_args(args)
2443 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002444
2445 # Access to a protected member _XX of a client class
2446 # pylint: disable=W0212
2447 try:
2448 import cpplint
2449 import cpplint_chromium
2450 except ImportError:
2451 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
2452 return 1
2453
2454 # Change the current working directory before calling lint so that it
2455 # shows the correct base.
2456 previous_cwd = os.getcwd()
2457 os.chdir(settings.GetRoot())
2458 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002459 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002460 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
2461 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00002462 if not files:
2463 print "Cannot lint an empty CL"
2464 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00002465
2466 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002467 command = args + files
2468 if options.filter:
2469 command = ['--filter=' + ','.join(options.filter)] + command
2470 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002471
2472 white_regex = re.compile(settings.GetLintRegex())
2473 black_regex = re.compile(settings.GetLintIgnoreRegex())
2474 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
2475 for filename in filenames:
2476 if white_regex.match(filename):
2477 if black_regex.match(filename):
2478 print "Ignoring file %s" % filename
2479 else:
2480 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
2481 extra_check_functions)
2482 else:
2483 print "Skipping file %s" % filename
2484 finally:
2485 os.chdir(previous_cwd)
2486 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
2487 if cpplint._cpplint_state.error_count != 0:
2488 return 1
2489 return 0
2490
2491
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002492def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002493 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002494 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002495 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002496 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00002497 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002498 auth.add_auth_options(parser)
2499 options, args = parser.parse_args(args)
2500 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002501
sbc@chromium.org71437c02015-04-09 19:29:40 +00002502 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00002503 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002504 return 1
2505
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002506 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002507 if args:
2508 base_branch = args[0]
2509 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002510 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002511 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002512
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002513 cl.RunHook(
2514 committing=not options.upload,
2515 may_prompt=False,
2516 verbose=options.verbose,
2517 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00002518 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002519
2520
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002521def AddChangeIdToCommitMessage(options, args):
2522 """Re-commits using the current message, assumes the commit hook is in
2523 place.
2524 """
2525 log_desc = options.message or CreateDescriptionFromLog(args)
2526 git_command = ['commit', '--amend', '-m', log_desc]
2527 RunGit(git_command)
2528 new_log_desc = CreateDescriptionFromLog(args)
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00002529 if git_footers.get_footer_change_id(new_log_desc):
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002530 print 'git-cl: Added Change-Id to commit message.'
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002531 return new_log_desc
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002532 else:
2533 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
2534
2535
tandrii@chromium.org65874e12016-03-04 12:03:02 +00002536def GenerateGerritChangeId(message):
2537 """Returns Ixxxxxx...xxx change id.
2538
2539 Works the same way as
2540 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
2541 but can be called on demand on all platforms.
2542
2543 The basic idea is to generate git hash of a state of the tree, original commit
2544 message, author/committer info and timestamps.
2545 """
2546 lines = []
2547 tree_hash = RunGitSilent(['write-tree'])
2548 lines.append('tree %s' % tree_hash.strip())
2549 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
2550 if code == 0:
2551 lines.append('parent %s' % parent.strip())
2552 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
2553 lines.append('author %s' % author.strip())
2554 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
2555 lines.append('committer %s' % committer.strip())
2556 lines.append('')
2557 # Note: Gerrit's commit-hook actually cleans message of some lines and
2558 # whitespace. This code is not doing this, but it clearly won't decrease
2559 # entropy.
2560 lines.append(message)
2561 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
2562 stdin='\n'.join(lines))
2563 return 'I%s' % change_hash.strip()
2564
2565
piman@chromium.org336f9122014-09-04 02:16:55 +00002566def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002567 """upload the current branch to gerrit."""
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002568 # TODO(tandrii): refactor this to be a method of _GerritChangelistImpl,
2569 # to avoid private members accessors below.
2570
tandrii@chromium.org95ffb612016-03-28 15:44:07 +00002571 # We assume the remote called "origin" is the one we want.
2572 # It is probably not worthwhile to support different workflows.
2573 gerrit_remote = 'origin'
2574
luqui@chromium.org609f3952015-05-04 22:47:04 +00002575 remote, remote_branch = cl.GetRemoteBranch()
2576 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2577 pending_prefix='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002578
andybons@chromium.org962f9462016-02-03 20:00:42 +00002579 if options.title:
2580 print "\nPatch titles (-t) are not supported in Gerrit. Aborting..."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002581 return 1
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002582
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002583 if options.squash:
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002584 if not cl.GetIssue():
2585 # TODO(tandrii): deperecate this after 2016Q2.
2586 # Backwards compatibility with shadow branch, which used to contain
2587 # change-id for a given branch, using which we can fetch actual issue
2588 # number and set it as the property of the branch, which is the new way.
2589 message = RunGitSilent(['show', '--format=%B', '-s',
2590 'refs/heads/git_cl_uploads/%s' % cl.GetBranch()])
2591 if message:
2592 change_ids = git_footers.get_footer_change_id(message.strip())
2593 if change_ids and len(change_ids) == 1:
2594 details = gerrit_util.GetChangeDetail(
2595 cl._codereview_impl._GetGerritHost(), change_ids[0])
2596 if details:
2597 print('WARNING: found old upload in branch git_cl_uploads/%s '
2598 'corresponding to issue %s' %
2599 (cl.GetBranch(), details['_number']))
2600 cl.SetIssue(details['_number'])
2601 if not cl.GetIssue():
2602 DieWithError(
2603 '\n' # For readability of the blob below.
2604 'Found old upload in branch git_cl_uploads/%s, '
2605 'but failed to find corresponding Gerrit issue.\n'
2606 'If you know the issue number, set it manually first:\n'
2607 ' git cl issue 123456\n'
2608 'If you intended to upload this CL as new issue, '
2609 'just delete or rename the old upload branch:\n'
2610 ' git rename-branch git_cl_uploads/%s old_upload-%s\n'
2611 'After that, please run git cl upload again.' %
2612 tuple([cl.GetBranch()] * 3))
2613 # End of backwards compatability.
2614
2615 if cl.GetIssue():
2616 # Try to get the message from a previous upload.
2617 message = cl.GetDescription()
2618 if not message:
2619 DieWithError(
2620 'failed to fetch description from current Gerrit issue %d\n'
2621 '%s' % (cl.GetIssue(), cl.GetIssueURL()))
2622 change_id = cl._codereview_impl._GetChangeDetail([])['change_id']
2623 while True:
2624 footer_change_ids = git_footers.get_footer_change_id(message)
2625 if footer_change_ids == [change_id]:
2626 break
2627 if not footer_change_ids:
2628 message = git_footers.add_footer_change_id(message, change_id)
2629 print('WARNING: appended missing Change-Id to issue description')
2630 continue
2631 # There is already a valid footer but with different or several ids.
2632 # Doing this automatically is non-trivial as we don't want to lose
2633 # existing other footers, yet we want to append just 1 desired
2634 # Change-Id. Thus, just create a new footer, but let user verify the new
2635 # description.
2636 message = '%s\n\nChange-Id: %s' % (message, change_id)
2637 print(
2638 'WARNING: issue %s has Change-Id footer(s):\n'
2639 ' %s\n'
2640 'but issue has Change-Id %s, according to Gerrit.\n'
2641 'Please, check the proposed correction to the description, '
2642 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2643 % (cl.GetIssue(), '\n '.join(footer_change_ids), change_id,
2644 change_id))
2645 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2646 if not options.force:
2647 change_desc = ChangeDescription(message)
2648 change_desc.prompt()
2649 message = change_desc.description
2650 if not message:
2651 DieWithError("Description is empty. Aborting...")
2652 # Continue the while loop.
2653 # Sanity check of this code - we should end up with proper message footer.
2654 assert [change_id] == git_footers.get_footer_change_id(message)
2655 change_desc = ChangeDescription(message)
2656 else:
2657 change_desc = ChangeDescription(
2658 options.message or CreateDescriptionFromLog(args))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002659 if not options.force:
2660 change_desc.prompt()
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002661 if not change_desc.description:
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002662 DieWithError("Description is empty. Aborting...")
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002663 message = change_desc.description
tandrii@chromium.org57d86542016-03-04 16:11:32 +00002664 change_ids = git_footers.get_footer_change_id(message)
2665 if len(change_ids) > 1:
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002666 DieWithError('too many Change-Id footers, at most 1 allowed.')
tandrii@chromium.org57d86542016-03-04 16:11:32 +00002667 if not change_ids:
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002668 # Generate the Change-Id automatically.
tandrii@chromium.org57d86542016-03-04 16:11:32 +00002669 message = git_footers.add_footer_change_id(
2670 message, GenerateGerritChangeId(message))
2671 change_desc.set_description(message)
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002672 change_ids = git_footers.get_footer_change_id(message)
2673 assert len(change_ids) == 1
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002674 change_id = change_ids[0]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002675
2676 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2677 if remote is '.':
2678 # If our upstream branch is local, we base our squashed commit on its
2679 # squashed version.
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002680 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2681 # Check the squashed hash of the parent.
2682 parent = RunGit(['config',
2683 'branch.%s.gerritsquashhash' % upstream_branch_name],
2684 error_ok=True).strip()
2685 # Verify that the upstream branch has been uploaded too, otherwise
2686 # Gerrit will create additional CLs when uploading.
2687 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2688 RunGitSilent(['rev-parse', parent + ':'])):
2689 # TODO(tandrii): remove "old depot_tools" part on April 12, 2016.
2690 DieWithError(
2691 'Upload upstream branch %s first.\n'
2692 'Note: maybe you\'ve uploaded it with --no-squash or with an old\n'
2693 ' version of depot_tools. If so, then re-upload it with:\n'
2694 ' git cl upload --squash\n' % upstream_branch_name)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002695 else:
2696 parent = cl.GetCommonAncestorWithUpstream()
2697
2698 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2699 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2700 '-m', message]).strip()
2701 else:
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002702 change_desc = ChangeDescription(
2703 options.message or CreateDescriptionFromLog(args))
2704 if not change_desc.description:
2705 DieWithError("Description is empty. Aborting...")
2706
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00002707 if not git_footers.get_footer_change_id(change_desc.description):
tandrii@chromium.org10625002016-03-04 20:03:47 +00002708 DownloadGerritHook(False)
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002709 change_desc.set_description(AddChangeIdToCommitMessage(options, args))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002710 ref_to_push = 'HEAD'
tandrii@chromium.org95ffb612016-03-28 15:44:07 +00002711 parent = '%s/%s' % (gerrit_remote, branch)
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002712 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002713
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002714 assert change_desc
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002715 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2716 ref_to_push)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002717 if len(commits) > 1:
2718 print('WARNING: This will upload %d commits. Run the following command '
2719 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002720 print('git log %s..%s' % (parent, ref_to_push))
2721 print('You can also use `git squash-branch` to squash these into a single '
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002722 'commit.')
2723 ask_for_data('About to upload; enter to confirm.')
2724
piman@chromium.org336f9122014-09-04 02:16:55 +00002725 if options.reviewers or options.tbr_owners:
2726 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002727
ukai@chromium.orge8077812012-02-03 03:41:46 +00002728 receive_options = []
2729 cc = cl.GetCCList().split(',')
2730 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002731 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002732 cc = filter(None, cc)
2733 if cc:
2734 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002735 if change_desc.get_reviewers():
2736 receive_options.extend(
2737 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002738
ukai@chromium.orge8077812012-02-03 03:41:46 +00002739 git_command = ['push']
2740 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00002741 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00002742 ' '.join(receive_options))
tandrii@chromium.org95ffb612016-03-28 15:44:07 +00002743 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002744 push_stdout = gclient_utils.CheckCallAndFilter(
2745 ['git'] + git_command,
2746 print_stdout=True,
2747 # Flush after every line: useful for seeing progress when running as
2748 # recipe.
2749 filter_fn=lambda _: sys.stdout.flush())
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002750
2751 if options.squash:
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002752 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2753 change_numbers = [m.group(1)
2754 for m in map(regex.match, push_stdout.splitlines())
2755 if m]
2756 if len(change_numbers) != 1:
2757 DieWithError(
2758 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2759 'Change-Id: %s') % (len(change_numbers), change_id))
2760 cl.SetIssue(change_numbers[0])
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002761 RunGit(['config', 'branch.%s.gerritsquashhash' % cl.GetBranch(),
2762 ref_to_push])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002763 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002764
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002765
wittman@chromium.org455dc922015-01-26 20:15:50 +00002766def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
2767 """Computes the remote branch ref to use for the CL.
2768
2769 Args:
2770 remote (str): The git remote for the CL.
2771 remote_branch (str): The git remote branch for the CL.
2772 target_branch (str): The target branch specified by the user.
2773 pending_prefix (str): The pending prefix from the settings.
2774 """
2775 if not (remote and remote_branch):
2776 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002777
wittman@chromium.org455dc922015-01-26 20:15:50 +00002778 if target_branch:
2779 # Cannonicalize branch references to the equivalent local full symbolic
2780 # refs, which are then translated into the remote full symbolic refs
2781 # below.
2782 if '/' not in target_branch:
2783 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
2784 else:
2785 prefix_replacements = (
2786 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
2787 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
2788 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
2789 )
2790 match = None
2791 for regex, replacement in prefix_replacements:
2792 match = re.search(regex, target_branch)
2793 if match:
2794 remote_branch = target_branch.replace(match.group(0), replacement)
2795 break
2796 if not match:
2797 # This is a branch path but not one we recognize; use as-is.
2798 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00002799 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
2800 # Handle the refs that need to land in different refs.
2801 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002802
wittman@chromium.org455dc922015-01-26 20:15:50 +00002803 # Create the true path to the remote branch.
2804 # Does the following translation:
2805 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
2806 # * refs/remotes/origin/master -> refs/heads/master
2807 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
2808 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
2809 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
2810 elif remote_branch.startswith('refs/remotes/%s/' % remote):
2811 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
2812 'refs/heads/')
2813 elif remote_branch.startswith('refs/remotes/branch-heads'):
2814 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
2815 # If a pending prefix exists then replace refs/ with it.
2816 if pending_prefix:
2817 remote_branch = remote_branch.replace('refs/', pending_prefix)
2818 return remote_branch
2819
2820
piman@chromium.org336f9122014-09-04 02:16:55 +00002821def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002822 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002823 upload_args = ['--assume_yes'] # Don't ask about untracked files.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002824 upload_args.extend(['--server', cl.GetCodereviewServer()])
2825 # TODO(tandrii): refactor this ugliness into _RietveldChangelistImpl.
2826 upload_args.extend(auth.auth_config_to_command_options(
2827 cl._codereview_impl.GetAuthConfig()))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002828 if options.emulate_svn_auto_props:
2829 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002830
2831 change_desc = None
2832
pgervais@chromium.org91141372014-01-09 23:27:20 +00002833 if options.email is not None:
2834 upload_args.extend(['--email', options.email])
2835
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002836 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002837 if options.title:
2838 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00002839 if options.message:
2840 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00002841 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002842 print ("This branch is associated with issue %s. "
2843 "Adding patch to that issue." % cl.GetIssue())
2844 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002845 if options.title:
2846 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00002847 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002848 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00002849 if options.reviewers or options.tbr_owners:
2850 change_desc.update_reviewers(options.reviewers,
2851 options.tbr_owners,
2852 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002853 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002854 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002855
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002856 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002857 print "Description is empty; aborting."
2858 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002859
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002860 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002861 if change_desc.get_reviewers():
2862 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00002863 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002864 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00002865 DieWithError("Must specify reviewers to send email.")
2866 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00002867
2868 # We check this before applying rietveld.private assuming that in
2869 # rietveld.cc only addresses which we can send private CLs to are listed
2870 # if rietveld.private is set, and so we should ignore rietveld.cc only when
2871 # --private is specified explicitly on the command line.
2872 if options.private:
2873 logging.warn('rietveld.cc is ignored since private flag is specified. '
2874 'You need to review and add them manually if necessary.')
2875 cc = cl.GetCCListWithoutDefault()
2876 else:
2877 cc = cl.GetCCList()
2878 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00002879 if cc:
2880 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002881
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002882 if options.private or settings.GetDefaultPrivateFlag() == "True":
2883 upload_args.append('--private')
2884
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002885 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00002886 if not options.find_copies:
2887 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002888
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002889 # Include the upstream repo's URL in the change -- this is useful for
2890 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002891 remote_url = cl.GetGitBaseUrlFromConfig()
2892 if not remote_url:
2893 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002894 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002895 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00002896 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
2897 remote_url = (cl.GetRemoteUrl() + '@'
2898 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002899 if remote_url:
2900 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00002901 remote, remote_branch = cl.GetRemoteBranch()
wittman@chromium.org455dc922015-01-26 20:15:50 +00002902 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2903 settings.GetPendingRefPrefix())
2904 if target_ref:
2905 upload_args.extend(['--target_ref', target_ref])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002906
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002907 # Look for dependent patchsets. See crbug.com/480453 for more details.
2908 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2909 upstream_branch = ShortBranchName(upstream_branch)
2910 if remote is '.':
2911 # A local branch is being tracked.
2912 local_branch = ShortBranchName(upstream_branch)
rmistry@google.com78948ed2015-07-08 23:09:57 +00002913 if settings.GetIsSkipDependencyUpload(local_branch):
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002914 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002915 print ('Skipping dependency patchset upload because git config '
2916 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002917 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002918 else:
2919 auth_config = auth.extract_auth_config_from_options(options)
2920 branch_cl = Changelist(branchref=local_branch, auth_config=auth_config)
2921 branch_cl_issue_url = branch_cl.GetIssueURL()
2922 branch_cl_issue = branch_cl.GetIssue()
2923 branch_cl_patchset = branch_cl.GetPatchset()
2924 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2925 upload_args.extend(
2926 ['--depends_on_patchset', '%s:%s' % (
2927 branch_cl_issue, branch_cl_patchset)])
2928 print
2929 print ('The current branch (%s) is tracking a local branch (%s) with '
2930 'an associated CL.') % (cl.GetBranch(), local_branch)
2931 print 'Adding %s/#ps%s as a dependency patchset.' % (
2932 branch_cl_issue_url, branch_cl_patchset)
2933 print
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002934
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002935 project = settings.GetProject()
2936 if project:
2937 upload_args.extend(['--project', project])
2938
rmistry@google.comef966222015-04-07 11:15:01 +00002939 if options.cq_dry_run:
2940 upload_args.extend(['--cq_dry_run'])
2941
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002942 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00002943 upload_args = ['upload'] + upload_args + args
2944 logging.info('upload.RealMain(%s)', upload_args)
2945 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00002946 issue = int(issue)
2947 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00002948 except KeyboardInterrupt:
2949 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002950 except:
2951 # If we got an exception after the user typed a description for their
2952 # change, back up the description before re-raising.
2953 if change_desc:
2954 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2955 print '\nGot exception while uploading -- saving description to %s\n' \
2956 % backup_path
2957 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002958 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002959 backup_file.close()
2960 raise
2961
2962 if not cl.GetIssue():
2963 cl.SetIssue(issue)
2964 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002965
2966 if options.use_commit_queue:
2967 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002968 return 0
2969
2970
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002971def cleanup_list(l):
2972 """Fixes a list so that comma separated items are put as individual items.
2973
2974 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
2975 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
2976 """
2977 items = sum((i.split(',') for i in l), [])
2978 stripped_items = (i.strip() for i in items)
2979 return sorted(filter(None, stripped_items))
2980
2981
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002982@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002983def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00002984 """Uploads the current changelist to codereview.
2985
2986 Can skip dependency patchset uploads for a branch by running:
2987 git config branch.branch_name.skip-deps-uploads True
2988 To unset run:
2989 git config --unset branch.branch_name.skip-deps-uploads
2990 Can also set the above globally by using the --global flag.
2991 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00002992 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2993 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002994 parser.add_option('--bypass-watchlists', action='store_true',
2995 dest='bypass_watchlists',
2996 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002997 parser.add_option('-f', action='store_true', dest='force',
2998 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002999 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003000 parser.add_option('-t', dest='title',
3001 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003002 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003003 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003004 help='reviewer email addresses')
3005 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003006 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003007 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003008 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003009 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003010 parser.add_option('--emulate_svn_auto_props',
3011 '--emulate-svn-auto-props',
3012 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003013 dest="emulate_svn_auto_props",
3014 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003015 parser.add_option('-c', '--use-commit-queue', action='store_true',
3016 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003017 parser.add_option('--private', action='store_true',
3018 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003019 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003020 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003021 metavar='TARGET',
3022 help='Apply CL to remote ref TARGET. ' +
3023 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003024 parser.add_option('--squash', action='store_true',
3025 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003026 parser.add_option('--no-squash', action='store_true',
3027 help='Don\'t squash multiple commits into one ' +
3028 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003029 parser.add_option('--email', default=None,
3030 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003031 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3032 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003033 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3034 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003035 help='Send the patchset to do a CQ dry run right after '
3036 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003037 parser.add_option('--dependencies', action='store_true',
3038 help='Uploads CLs of all the local branches that depend on '
3039 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003040
rmistry@google.com2dd99862015-06-22 12:22:18 +00003041 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003042 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003043 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003044 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003045 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003046
sbc@chromium.org71437c02015-04-09 19:29:40 +00003047 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003048 return 1
3049
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003050 options.reviewers = cleanup_list(options.reviewers)
3051 options.cc = cleanup_list(options.cc)
3052
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003053 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3054 settings.GetIsGerrit()
3055
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003056 cl = Changelist(auth_config=auth_config)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003057 if args:
3058 # TODO(ukai): is it ok for gerrit case?
3059 base_branch = args[0]
3060 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00003061 if cl.GetBranch() is None:
3062 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
3063
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003064 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003065 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00003066 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00003067
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003068 # Make sure authenticated to Rietveld before running expensive hooks. It is
3069 # a fast, best efforts check. Rietveld still can reject the authentication
3070 # during the actual upload.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003071 if not cl.IsGerrit() and auth_config.use_oauth2:
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003072 authenticator = auth.get_authenticator_for_host(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003073 cl.GetCodereviewServer(), auth_config)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003074 if not authenticator.has_cached_credentials():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003075 raise auth.LoginRequiredError(cl.GetCodereviewServer())
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003076
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003077 # Apply watchlists on upload.
3078 change = cl.GetChange(base_branch, None)
3079 watchlist = watchlists.Watchlists(change.RepositoryRoot())
3080 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003081 if not options.bypass_watchlists:
3082 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003083
ukai@chromium.orge8077812012-02-03 03:41:46 +00003084 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00003085 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00003086 # Set the reviewer list now so that presubmit checks can access it.
3087 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00003088 change_description.update_reviewers(options.reviewers,
3089 options.tbr_owners,
3090 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00003091 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003092 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00003093 may_prompt=not options.force,
3094 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003095 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003096 if not hook_results.should_continue():
3097 return 1
3098 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003099 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003100
koz@chromium.org5974d7a2013-04-02 20:50:37 +00003101 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003102 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00003103 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00003104 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00003105 print ('The last upload made from this repository was patchset #%d but '
3106 'the most recent patchset on the server is #%d.'
3107 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00003108 print ('Uploading will still work, but if you\'ve uploaded to this issue '
3109 'from another machine or branch the patch you\'re uploading now '
3110 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00003111 ask_for_data('About to upload; enter to confirm.')
3112
iannucci@chromium.org79540052012-10-19 23:15:26 +00003113 print_stats(options.similarity, options.find_copies, args)
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003114 if cl.IsGerrit():
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003115 if options.squash and options.no_squash:
3116 DieWithError('Can only use one of --squash or --no-squash')
3117
3118 options.squash = ((settings.GetSquashGerritUploads() or options.squash) and
3119 not options.no_squash)
3120
tandrii@chromium.org1e67bb72016-02-11 12:15:49 +00003121 ret = GerritUpload(options, args, cl, change)
3122 else:
3123 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00003124 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00003125 git_set_branch_value('last-upload-hash',
3126 RunGit(['rev-parse', 'HEAD']).strip())
rmistry@google.com5626a922015-02-26 14:03:30 +00003127 # Run post upload hooks, if specified.
3128 if settings.GetRunPostUploadHook():
3129 presubmit_support.DoPostUploadExecuter(
3130 change,
3131 cl,
3132 settings.GetRoot(),
3133 options.verbose,
3134 sys.stdout)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00003135
rmistry@google.com2dd99862015-06-22 12:22:18 +00003136 # Upload all dependencies if specified.
3137 if options.dependencies:
3138 print
3139 print '--dependencies has been specified.'
3140 print 'All dependent local branches will be re-uploaded.'
3141 print
3142 # Remove the dependencies flag from args so that we do not end up in a
3143 # loop.
3144 orig_args.remove('--dependencies')
3145 upload_branch_deps(cl, orig_args)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00003146 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00003147
3148
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003149def IsSubmoduleMergeCommit(ref):
3150 # When submodules are added to the repo, we expect there to be a single
3151 # non-git-svn merge commit at remote HEAD with a signature comment.
3152 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003153 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003154 return RunGit(cmd) != ''
3155
3156
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003157def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003158 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003159
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003160 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3161 upstream and closes the issue automatically and atomically.
3162
3163 Otherwise (in case of Rietveld):
3164 Squashes branch into a single commit.
3165 Updates changelog with metadata (e.g. pointer to review).
3166 Pushes/dcommits the code upstream.
3167 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003168 """
3169 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3170 help='bypass upload presubmit hook')
3171 parser.add_option('-m', dest='message',
3172 help="override review description")
3173 parser.add_option('-f', action='store_true', dest='force',
3174 help="force yes to questions (don't prompt)")
3175 parser.add_option('-c', dest='contributor',
3176 help="external contributor for patch (appended to " +
3177 "description and used as author for git). Should be " +
3178 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003179 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003180 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003181 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003182 auth_config = auth.extract_auth_config_from_options(options)
3183
3184 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003185
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003186 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3187 if cl.IsGerrit():
3188 if options.message:
3189 # This could be implemented, but it requires sending a new patch to
3190 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3191 # Besides, Gerrit has the ability to change the commit message on submit
3192 # automatically, thus there is no need to support this option (so far?).
3193 parser.error('-m MESSAGE option is not supported for Gerrit.')
3194 if options.contributor:
3195 parser.error(
3196 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3197 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3198 'the contributor\'s "name <email>". If you can\'t upload such a '
3199 'commit for review, contact your repository admin and request'
3200 '"Forge-Author" permission.')
3201 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3202 options.verbose)
3203
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003204 current = cl.GetBranch()
3205 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3206 if not settings.GetIsGitSvn() and remote == '.':
3207 print
3208 print 'Attempting to push branch %r into another local branch!' % current
3209 print
3210 print 'Either reparent this branch on top of origin/master:'
3211 print ' git reparent-branch --root'
3212 print
3213 print 'OR run `git rebase-update` if you think the parent branch is already'
3214 print 'committed.'
3215 print
3216 print ' Current parent: %r' % upstream_branch
3217 return 1
3218
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003219 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003220 # Default to merging against our best guess of the upstream branch.
3221 args = [cl.GetUpstreamBranch()]
3222
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003223 if options.contributor:
3224 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
3225 print "Please provide contibutor as 'First Last <email@example.com>'"
3226 return 1
3227
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003228 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003229 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003230
sbc@chromium.org71437c02015-04-09 19:29:40 +00003231 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003232 return 1
3233
3234 # This rev-list syntax means "show all commits not in my branch that
3235 # are in base_branch".
3236 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3237 base_branch]).splitlines()
3238 if upstream_commits:
3239 print ('Base branch "%s" has %d commits '
3240 'not in this branch.' % (base_branch, len(upstream_commits)))
3241 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
3242 return 1
3243
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003244 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003245 svn_head = None
3246 if cmd == 'dcommit' or base_has_submodules:
3247 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
3248 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003249
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003250 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003251 # If the base_head is a submodule merge commit, the first parent of the
3252 # base_head should be a git-svn commit, which is what we're interested in.
3253 base_svn_head = base_branch
3254 if base_has_submodules:
3255 base_svn_head += '^1'
3256
3257 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003258 if extra_commits:
3259 print ('This branch has %d additional commits not upstreamed yet.'
3260 % len(extra_commits.splitlines()))
3261 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
3262 'before attempting to %s.' % (base_branch, cmd))
3263 return 1
3264
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003265 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003266 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003267 author = None
3268 if options.contributor:
3269 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003270 hook_results = cl.RunHook(
3271 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003272 may_prompt=not options.force,
3273 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003274 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003275 if not hook_results.should_continue():
3276 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003277
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003278 # Check the tree status if the tree status URL is set.
3279 status = GetTreeStatus()
3280 if 'closed' == status:
3281 print('The tree is closed. Please wait for it to reopen. Use '
3282 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3283 return 1
3284 elif 'unknown' == status:
3285 print('Unable to determine tree status. Please verify manually and '
3286 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3287 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003288
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003289 change_desc = ChangeDescription(options.message)
3290 if not change_desc.description and cl.GetIssue():
3291 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003292
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003293 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00003294 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003295 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00003296 else:
3297 print 'No description set.'
3298 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
3299 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003300
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003301 # Keep a separate copy for the commit message, because the commit message
3302 # contains the link to the Rietveld issue, while the Rietveld message contains
3303 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003304 # Keep a separate copy for the commit message.
3305 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00003306 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003307
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003308 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00003309 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00003310 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00003311 # after it. Add a period on a new line to circumvent this. Also add a space
3312 # before the period to make sure that Gitiles continues to correctly resolve
3313 # the URL.
3314 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003315 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003316 commit_desc.append_footer('Patch from %s.' % options.contributor)
3317
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00003318 print('Description:')
3319 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003320
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003321 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003322 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00003323 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003324
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003325 # We want to squash all this branch's commits into one commit with the proper
3326 # description. We do this by doing a "reset --soft" to the base branch (which
3327 # keeps the working copy the same), then dcommitting that. If origin/master
3328 # has a submodule merge commit, we'll also need to cherry-pick the squashed
3329 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003330 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003331 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
3332 # Delete the branches if they exist.
3333 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
3334 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
3335 result = RunGitWithCode(showref_cmd)
3336 if result[0] == 0:
3337 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003338
3339 # We might be in a directory that's present in this branch but not in the
3340 # trunk. Move up to the top of the tree so that git commands that expect a
3341 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003342 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003343 if rel_base_path:
3344 os.chdir(rel_base_path)
3345
3346 # Stuff our change into the merge branch.
3347 # We wrap in a try...finally block so if anything goes wrong,
3348 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003349 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003350 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003351 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003352 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003353 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00003354 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003355 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003356 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003357 RunGit(
3358 [
3359 'commit', '--author', options.contributor,
3360 '-m', commit_desc.description,
3361 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003362 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003363 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003364 if base_has_submodules:
3365 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
3366 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
3367 RunGit(['checkout', CHERRY_PICK_BRANCH])
3368 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003369 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003370 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003371 mirror = settings.GetGitMirror(remote)
3372 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003373 pending_prefix = settings.GetPendingRefPrefix()
3374 if not pending_prefix or branch.startswith(pending_prefix):
3375 # If not using refs/pending/heads/* at all, or target ref is already set
3376 # to pending, then push to the target ref directly.
3377 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003378 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003379 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003380 else:
3381 # Cherry-pick the change on top of pending ref and then push it.
3382 assert branch.startswith('refs/'), branch
3383 assert pending_prefix[-1] == '/', pending_prefix
3384 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003385 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003386 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003387 if retcode == 0:
3388 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003389 else:
3390 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003391 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003392 'svn', 'dcommit',
3393 '-C%s' % options.similarity,
3394 '--no-rebase', '--rmdir',
3395 ]
3396 if settings.GetForceHttpsCommitUrl():
3397 # Allow forcing https commit URLs for some projects that don't allow
3398 # committing to http URLs (like Google Code).
3399 remote_url = cl.GetGitSvnRemoteUrl()
3400 if urlparse.urlparse(remote_url).scheme == 'http':
3401 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003402 cmd_args.append('--commit-url=%s' % remote_url)
3403 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003404 if 'Committed r' in output:
3405 revision = re.match(
3406 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
3407 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003408 finally:
3409 # And then swap back to the original branch and clean up.
3410 RunGit(['checkout', '-q', cl.GetBranch()])
3411 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003412 if base_has_submodules:
3413 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003414
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003415 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003416 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003417 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003418
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003419 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003420 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003421 try:
3422 revision = WaitForRealCommit(remote, revision, base_branch, branch)
3423 # We set pushed_to_pending to False, since it made it all the way to the
3424 # real ref.
3425 pushed_to_pending = False
3426 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003427 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003428
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003429 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003430 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003431 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003432 if not to_pending:
3433 if viewvc_url and revision:
3434 change_desc.append_footer(
3435 'Committed: %s%s' % (viewvc_url, revision))
3436 elif revision:
3437 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003438 print ('Closing issue '
3439 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003440 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003441 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003442 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00003443 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00003444 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00003445 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003446 if options.bypass_hooks:
3447 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
3448 else:
3449 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00003450 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003451 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003452
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003453 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003454 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
3455 print 'The commit is in the pending queue (%s).' % pending_ref
3456 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00003457 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003458 'footer.' % branch)
3459
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003460 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
3461 if os.path.isfile(hook):
3462 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003463
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003464 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003465
3466
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003467def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
3468 print
3469 print 'Waiting for commit to be landed on %s...' % real_ref
3470 print '(If you are impatient, you may Ctrl-C once without harm)'
3471 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
3472 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003473 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003474
3475 loop = 0
3476 while True:
3477 sys.stdout.write('fetching (%d)... \r' % loop)
3478 sys.stdout.flush()
3479 loop += 1
3480
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003481 if mirror:
3482 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003483 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
3484 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
3485 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
3486 for commit in commits.splitlines():
3487 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
3488 print 'Found commit on %s' % real_ref
3489 return commit
3490
3491 current_rev = to_rev
3492
3493
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003494def PushToGitPending(remote, pending_ref, upstream_ref):
3495 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
3496
3497 Returns:
3498 (retcode of last operation, output log of last operation).
3499 """
3500 assert pending_ref.startswith('refs/'), pending_ref
3501 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
3502 cherry = RunGit(['rev-parse', 'HEAD']).strip()
3503 code = 0
3504 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003505 max_attempts = 3
3506 attempts_left = max_attempts
3507 while attempts_left:
3508 if attempts_left != max_attempts:
3509 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
3510 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003511
3512 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003513 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003514 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003515 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003516 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003517 print 'Fetch failed with exit code %d.' % code
3518 if out.strip():
3519 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003520 continue
3521
3522 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003523 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003524 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003525 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003526 if code:
3527 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003528 'Your patch doesn\'t apply cleanly to ref \'%s\', '
3529 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003530 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
3531 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003532 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003533 return code, out
3534
3535 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003536 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003537 code, out = RunGitWithCode(
3538 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
3539 if code == 0:
3540 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003541 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003542 return code, out
3543
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003544 print 'Push failed with exit code %d.' % code
3545 if out.strip():
3546 print out.strip()
3547 if IsFatalPushFailure(out):
3548 print (
3549 'Fatal push error. Make sure your .netrc credentials and git '
3550 'user.email are correct and you have push access to the repo.')
3551 return code, out
3552
3553 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003554 return code, out
3555
3556
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003557def IsFatalPushFailure(push_stdout):
3558 """True if retrying push won't help."""
3559 return '(prohibited by Gerrit)' in push_stdout
3560
3561
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003562@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003563def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003564 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003565 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003566 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003567 # If it looks like previous commits were mirrored with git-svn.
3568 message = """This repository appears to be a git-svn mirror, but no
3569upstream SVN master is set. You probably need to run 'git auto-svn' once."""
3570 else:
3571 message = """This doesn't appear to be an SVN repository.
3572If your project has a true, writeable git repository, you probably want to run
3573'git cl land' instead.
3574If your project has a git mirror of an upstream SVN master, you probably need
3575to run 'git svn init'.
3576
3577Using the wrong command might cause your commit to appear to succeed, and the
3578review to be closed, without actually landing upstream. If you choose to
3579proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00003580 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00003581 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003582 return SendUpstream(parser, args, 'dcommit')
3583
3584
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003585@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003586def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003587 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003588 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003589 print('This appears to be an SVN repository.')
3590 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003591 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00003592 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003593 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003594
3595
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003596def ParseIssueNum(arg):
3597 """Parses the issue number from args if present otherwise returns None."""
3598 if re.match(r'\d+', arg):
3599 return arg
3600 if arg.startswith('http'):
3601 return re.sub(r'.*/(\d+)/?', r'\1', arg)
3602 return None
3603
3604
3605@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003606def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00003607 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003608 parser.add_option('-b', dest='newbranch',
3609 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003610 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003611 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003612 parser.add_option('-d', '--directory', action='store', metavar='DIR',
3613 help='Change to the directory DIR immediately, '
3614 'before doing anything else.')
3615 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003616 help='failed patches spew .rej files rather than '
3617 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003618 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
3619 help="don't commit after patch applies")
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003620
3621 group = optparse.OptionGroup(parser,
3622 """Options for continuing work on the current issue uploaded
3623from a different clone (e.g. different machine). Must be used independently from
3624the other options. No issue number should be specified, and the branch must have
3625an issue number associated with it""")
3626 group.add_option('--reapply', action='store_true',
3627 dest='reapply',
3628 help="""Reset the branch and reapply the issue.
3629CAUTION: This will undo any local changes in this branch""")
3630
3631 group.add_option('--pull', action='store_true', dest='pull',
3632 help="Performs a pull before reapplying.")
3633 parser.add_option_group(group)
3634
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003635 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003636 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003637 auth_config = auth.extract_auth_config_from_options(options)
3638
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003639 issue_arg = None
3640 if options.reapply :
3641 if len(args) > 0:
3642 parser.error("--reapply implies no additional arguments.")
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003643
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003644 cl = Changelist()
3645 issue_arg = cl.GetIssue()
3646 upstream = cl.GetUpstreamBranch()
3647 if upstream == None:
3648 parser.error("No upstream branch specified. Cannot reset branch")
3649
3650 RunGit(['reset', '--hard', upstream])
3651 if options.pull:
3652 RunGit(['pull'])
3653 else:
3654 if len(args) != 1:
3655 parser.error("Must specify issue number")
3656
3657 issue_arg = ParseIssueNum(args[0])
3658
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003659 # The patch URL works because ParseIssueNum won't do any substitution
3660 # as the re.sub pattern fails to match and just returns it.
3661 if issue_arg == None:
3662 parser.print_help()
3663 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003664
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003665 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003666 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003667 return 1
3668
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003669 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00003670 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003671
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003672 if options.newbranch:
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003673 if options.reapply:
3674 parser.error("--reapply excludes any option other than --pull")
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003675 if options.force:
3676 RunGit(['branch', '-D', options.newbranch],
3677 stderr=subprocess2.PIPE, error_ok=True)
3678 RunGit(['checkout', '-b', options.newbranch,
3679 Changelist().GetUpstreamBranch()])
3680
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003681 return PatchIssue(issue_arg, options.reject, options.nocommit,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003682 options.directory, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003683
3684
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003685def PatchIssue(issue_arg, reject, nocommit, directory, auth_config):
wychen@chromium.orga872e752015-04-28 23:42:18 +00003686 # PatchIssue should never be called with a dirty tree. It is up to the
3687 # caller to check this, but just in case we assert here since the
3688 # consequences of the caller not checking this could be dire.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003689 assert(not git_common.is_dirty_git_tree('apply'))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003690
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003691 # TODO(tandrii): implement for Gerrit.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003692 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003693 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00003694 issue = int(issue_arg)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003695 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003696 patchset = cl.GetMostRecentPatchset()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003697 patch_data = cl._codereview_impl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003698 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00003699 # Assume it's a URL to the patch. Default to https.
3700 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
kjellander@chromium.org44424542015-06-02 18:35:29 +00003701 match = re.match(r'(.*?)/download/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00003702 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003703 DieWithError('Must pass an issue ID or full URL for '
3704 '\'Download raw patch set\'')
kjellander@chromium.org44424542015-06-02 18:35:29 +00003705 issue = int(match.group(2))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003706 cl = Changelist(issue=issue, codereview='rietveld',
tandrii@chromium.orgbf8f7512016-04-01 08:40:57 +00003707 rietveld_server=match.group(1), auth_config=auth_config)
kjellander@chromium.org44424542015-06-02 18:35:29 +00003708 patchset = int(match.group(3))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00003709 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003710
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003711 # Switch up to the top-level directory, if necessary, in preparation for
3712 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003713 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003714 if top:
3715 os.chdir(top)
3716
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003717 # Git patches have a/ at the beginning of source paths. We strip that out
3718 # with a sed script rather than the -p flag to patch so we can feed either
3719 # Git or svn-style patches into the same apply command.
3720 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003721 try:
3722 patch_data = subprocess2.check_output(
3723 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
3724 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003725 DieWithError('Git patch mungling failed.')
3726 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003727
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003728 # We use "git apply" to apply the patch instead of "patch" so that we can
3729 # pick up file adds.
3730 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003731 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003732 if directory:
3733 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003734 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003735 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003736 elif IsGitVersionAtLeast('1.7.12'):
3737 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003738 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003739 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003740 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003741 except subprocess2.CalledProcessError:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003742 print 'Failed to apply the patch'
3743 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003744
3745 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003746 if not nocommit:
wychen@chromium.org5b3bebb2015-05-28 21:41:43 +00003747 RunGit(['commit', '-m', (cl.GetDescription() + '\n\n' +
3748 'patch from issue %(i)s at patchset '
carlosk@chromium.org71284d92014-11-14 18:12:50 +00003749 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
3750 % {'i': issue, 'p': patchset})])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003751 cl = Changelist(codereview='rietveld', auth_config=auth_config,
3752 rietveld_server=cl.GetCodereviewServer())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003753 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00003754 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00003755 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003756 else:
3757 print "Patch applied to index."
3758 return 0
3759
3760
3761def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003762 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003763 # Provide a wrapper for git svn rebase to help avoid accidental
3764 # git svn dcommit.
3765 # It's the only command that doesn't use parser at all since we just defer
3766 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003767
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003768 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003769
3770
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003771def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003772 """Fetches the tree status and returns either 'open', 'closed',
3773 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003774 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003775 if url:
3776 status = urllib2.urlopen(url).read().lower()
3777 if status.find('closed') != -1 or status == '0':
3778 return 'closed'
3779 elif status.find('open') != -1 or status == '1':
3780 return 'open'
3781 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003782 return 'unset'
3783
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003784
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003785def GetTreeStatusReason():
3786 """Fetches the tree status from a json url and returns the message
3787 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00003788 url = settings.GetTreeStatusUrl()
3789 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003790 connection = urllib2.urlopen(json_url)
3791 status = json.loads(connection.read())
3792 connection.close()
3793 return status['message']
3794
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003795
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003796def GetBuilderMaster(bot_list):
3797 """For a given builder, fetch the master from AE if available."""
3798 map_url = 'https://builders-map.appspot.com/'
3799 try:
3800 master_map = json.load(urllib2.urlopen(map_url))
3801 except urllib2.URLError as e:
3802 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
3803 (map_url, e))
3804 except ValueError as e:
3805 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
3806 if not master_map:
3807 return None, 'Failed to build master map.'
3808
3809 result_master = ''
3810 for bot in bot_list:
3811 builder = bot.split(':', 1)[0]
3812 master_list = master_map.get(builder, [])
3813 if not master_list:
3814 return None, ('No matching master for builder %s.' % builder)
3815 elif len(master_list) > 1:
3816 return None, ('The builder name %s exists in multiple masters %s.' %
3817 (builder, master_list))
3818 else:
3819 cur_master = master_list[0]
3820 if not result_master:
3821 result_master = cur_master
3822 elif result_master != cur_master:
3823 return None, 'The builders do not belong to the same master.'
3824 return result_master, None
3825
3826
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003827def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003828 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003829 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003830 status = GetTreeStatus()
3831 if 'unset' == status:
3832 print 'You must configure your tree status URL by running "git cl config".'
3833 return 2
3834
3835 print "The tree is %s" % status
3836 print
3837 print GetTreeStatusReason()
3838 if status != 'open':
3839 return 1
3840 return 0
3841
3842
maruel@chromium.org15192402012-09-06 12:38:29 +00003843def CMDtry(parser, args):
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003844 """Triggers a try job through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00003845 group = optparse.OptionGroup(parser, "Try job options")
3846 group.add_option(
3847 "-b", "--bot", action="append",
3848 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
3849 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003850 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00003851 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003852 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00003853 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003854 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00003855 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003856 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003857 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003858 "-r", "--revision",
3859 help="Revision to use for the try job; default: the "
3860 "revision will be determined by the try server; see "
3861 "its waterfall for more info")
3862 group.add_option(
3863 "-c", "--clobber", action="store_true", default=False,
3864 help="Force a clobber before building; e.g. don't do an "
3865 "incremental build")
3866 group.add_option(
3867 "--project",
3868 help="Override which project to use. Projects are defined "
3869 "server-side to define what default bot set to use")
3870 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00003871 "-p", "--property", dest="properties", action="append", default=[],
3872 help="Specify generic properties in the form -p key1=value1 -p "
3873 "key2=value2 etc (buildbucket only). The value will be treated as "
3874 "json if decodable, or as string otherwise.")
3875 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003876 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003877 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003878 "--use-rietveld", action="store_true", default=False,
3879 help="Use Rietveld to trigger try jobs.")
3880 group.add_option(
3881 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3882 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00003883 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003884 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00003885 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003886 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00003887
machenbach@chromium.org45453142015-09-15 08:45:22 +00003888 if options.use_rietveld and options.properties:
3889 parser.error('Properties can only be specified with buildbucket')
3890
3891 # Make sure that all properties are prop=value pairs.
3892 bad_params = [x for x in options.properties if '=' not in x]
3893 if bad_params:
3894 parser.error('Got properties with missing "=": %s' % bad_params)
3895
maruel@chromium.org15192402012-09-06 12:38:29 +00003896 if args:
3897 parser.error('Unknown arguments: %s' % args)
3898
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003899 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00003900 if not cl.GetIssue():
3901 parser.error('Need to upload first')
3902
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003903 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00003904 if props.get('closed'):
3905 parser.error('Cannot send tryjobs for a closed CL')
3906
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003907 if props.get('private'):
3908 parser.error('Cannot use trybots with private issue')
3909
maruel@chromium.org15192402012-09-06 12:38:29 +00003910 if not options.name:
3911 options.name = cl.GetBranch()
3912
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003913 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003914 options.master, err_msg = GetBuilderMaster(options.bot)
3915 if err_msg:
3916 parser.error('Tryserver master cannot be found because: %s\n'
3917 'Please manually specify the tryserver master'
3918 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003919
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003920 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003921 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003922 if not options.bot:
3923 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00003924
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003925 # Get try masters from PRESUBMIT.py files.
3926 masters = presubmit_support.DoGetTryMasters(
3927 change,
3928 change.LocalPaths(),
3929 settings.GetRoot(),
3930 None,
3931 None,
3932 options.verbose,
3933 sys.stdout)
3934 if masters:
3935 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00003936
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003937 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
3938 options.bot = presubmit_support.DoGetTrySlaves(
3939 change,
3940 change.LocalPaths(),
3941 settings.GetRoot(),
3942 None,
3943 None,
3944 options.verbose,
3945 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00003946
3947 if not options.bot:
3948 # Get try masters from cq.cfg if any.
3949 # TODO(tandrii): some (but very few) projects store cq.cfg in different
3950 # location.
3951 cq_cfg = os.path.join(change.RepositoryRoot(),
3952 'infra', 'config', 'cq.cfg')
3953 if os.path.exists(cq_cfg):
3954 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00003955 cq_masters = commit_queue.get_master_builder_map(
3956 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00003957 for master, builders in cq_masters.iteritems():
3958 for builder in builders:
3959 # Skip presubmit builders, because these will fail without LGTM.
3960 if 'presubmit' not in builder.lower():
3961 masters.setdefault(master, {})[builder] = ['defaulttests']
3962 if masters:
3963 return masters
3964
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003965 if not options.bot:
3966 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00003967
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003968 builders_and_tests = {}
3969 # TODO(machenbach): The old style command-line options don't support
3970 # multiple try masters yet.
3971 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
3972 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
3973
3974 for bot in old_style:
3975 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003976 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003977 elif ',' in bot:
3978 parser.error('Specify one bot per --bot flag')
3979 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00003980 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003981
3982 for bot, tests in new_style:
3983 builders_and_tests.setdefault(bot, []).extend(tests)
3984
3985 # Return a master map with one master to be backwards compatible. The
3986 # master name defaults to an empty string, which will cause the master
3987 # not to be set on rietveld (deprecated).
3988 return {options.master: builders_and_tests}
3989
3990 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00003991
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003992 for builders in masters.itervalues():
3993 if any('triggered' in b for b in builders):
3994 print >> sys.stderr, (
3995 'ERROR You are trying to send a job to a triggered bot. This type of'
3996 ' bot requires an\ninitial job from a parent (usually a builder). '
3997 'Instead send your job to the parent.\n'
3998 'Bot list: %s' % builders)
3999 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004000
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004001 patchset = cl.GetMostRecentPatchset()
4002 if patchset and patchset != cl.GetPatchset():
4003 print(
4004 '\nWARNING Mismatch between local config and server. Did a previous '
4005 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4006 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004007 if options.luci:
4008 trigger_luci_job(cl, masters, options)
4009 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004010 try:
4011 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4012 except BuildbucketResponseException as ex:
4013 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004014 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004015 except Exception as e:
4016 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4017 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
4018 e, stacktrace)
4019 return 1
4020 else:
4021 try:
4022 cl.RpcServer().trigger_distributed_try_jobs(
4023 cl.GetIssue(), patchset, options.name, options.clobber,
4024 options.revision, masters)
4025 except urllib2.HTTPError as e:
4026 if e.code == 404:
4027 print('404 from rietveld; '
4028 'did you mean to use "git try" instead of "git cl try"?')
4029 return 1
4030 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004031
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004032 for (master, builders) in sorted(masters.iteritems()):
4033 if master:
4034 print 'Master: %s' % master
4035 length = max(len(builder) for builder in builders)
4036 for builder in sorted(builders):
4037 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00004038 return 0
4039
4040
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004041def CMDtry_results(parser, args):
4042 group = optparse.OptionGroup(parser, "Try job results options")
4043 group.add_option(
4044 "-p", "--patchset", type=int, help="patchset number if not current.")
4045 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004046 "--print-master", action='store_true', help="print master name as well.")
4047 group.add_option(
4048 "--color", action='store_true', default=sys.stdout.isatty(),
4049 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004050 group.add_option(
4051 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4052 help="Host of buildbucket. The default host is %default.")
4053 parser.add_option_group(group)
4054 auth.add_auth_options(parser)
4055 options, args = parser.parse_args(args)
4056 if args:
4057 parser.error('Unrecognized args: %s' % ' '.join(args))
4058
4059 auth_config = auth.extract_auth_config_from_options(options)
4060 cl = Changelist(auth_config=auth_config)
4061 if not cl.GetIssue():
4062 parser.error('Need to upload first')
4063
4064 if not options.patchset:
4065 options.patchset = cl.GetMostRecentPatchset()
4066 if options.patchset and options.patchset != cl.GetPatchset():
4067 print(
4068 '\nWARNING Mismatch between local config and server. Did a previous '
4069 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4070 'Continuing using\npatchset %s.\n' % options.patchset)
4071 try:
4072 jobs = fetch_try_jobs(auth_config, cl, options)
4073 except BuildbucketResponseException as ex:
4074 print 'Buildbucket error: %s' % ex
4075 return 1
4076 except Exception as e:
4077 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4078 print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
4079 e, stacktrace)
4080 return 1
4081 print_tryjobs(options, jobs)
4082 return 0
4083
4084
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004085@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004086def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004087 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004088 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004089 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004090 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004091
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004092 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004093 if args:
4094 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004095 branch = cl.GetBranch()
4096 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004097 cl = Changelist()
4098 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004099
4100 # Clear configured merge-base, if there is one.
4101 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004102 else:
4103 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004104 return 0
4105
4106
thestig@chromium.org00858c82013-12-02 23:08:03 +00004107def CMDweb(parser, args):
4108 """Opens the current CL in the web browser."""
4109 _, args = parser.parse_args(args)
4110 if args:
4111 parser.error('Unrecognized args: %s' % ' '.join(args))
4112
4113 issue_url = Changelist().GetIssueURL()
4114 if not issue_url:
4115 print >> sys.stderr, 'ERROR No issue to open'
4116 return 1
4117
4118 webbrowser.open(issue_url)
4119 return 0
4120
4121
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004122def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004123 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004124 auth.add_auth_options(parser)
4125 options, args = parser.parse_args(args)
4126 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004127 if args:
4128 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004129 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004130 props = cl.GetIssueProperties()
4131 if props.get('private'):
4132 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004133 cl.SetFlag('commit', '1')
4134 return 0
4135
4136
groby@chromium.org411034a2013-02-26 15:12:01 +00004137def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004138 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004139 auth.add_auth_options(parser)
4140 options, args = parser.parse_args(args)
4141 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004142 if args:
4143 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004144 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00004145 # Ensure there actually is an issue to close.
4146 cl.GetDescription()
4147 cl.CloseIssue()
4148 return 0
4149
4150
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004151def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004152 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004153 auth.add_auth_options(parser)
4154 options, args = parser.parse_args(args)
4155 auth_config = auth.extract_auth_config_from_options(options)
4156 if args:
4157 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004158
4159 # Uncommitted (staged and unstaged) changes will be destroyed by
4160 # "git reset --hard" if there are merging conflicts in PatchIssue().
4161 # Staged changes would be committed along with the patch from last
4162 # upload, hence counted toward the "last upload" side in the final
4163 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004164 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004165 return 1
4166
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004167 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004168 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004169 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004170 if not issue:
4171 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004172 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004173 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004174
4175 # Create a new branch based on the merge-base
4176 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
4177 try:
4178 # Patch in the latest changes from rietveld.
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004179 rtn = PatchIssue(issue, False, False, None, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004180 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004181 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004182 return rtn
4183
wychen@chromium.org06928532015-02-03 02:11:29 +00004184 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004185 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00004186 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004187 finally:
4188 RunGit(['checkout', '-q', branch])
4189 RunGit(['branch', '-D', TMP_BRANCH])
4190
4191 return 0
4192
4193
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004194def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004195 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004196 parser.add_option(
4197 '--no-color',
4198 action='store_true',
4199 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004200 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004201 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004202 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004203
4204 author = RunGit(['config', 'user.email']).strip() or None
4205
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004206 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004207
4208 if args:
4209 if len(args) > 1:
4210 parser.error('Unknown args')
4211 base_branch = args[0]
4212 else:
4213 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004214 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004215
4216 change = cl.GetChange(base_branch, None)
4217 return owners_finder.OwnersFinder(
4218 [f.LocalPath() for f in
4219 cl.GetChange(base_branch, None).AffectedFiles()],
4220 change.RepositoryRoot(), author,
4221 fopen=file, os_path=os.path, glob=glob.glob,
4222 disable_color=options.no_color).run()
4223
4224
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004225def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004226 """Generates a diff command."""
4227 # Generate diff for the current branch's changes.
4228 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4229 upstream_commit, '--' ]
4230
4231 if args:
4232 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004233 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004234 diff_cmd.append(arg)
4235 else:
4236 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004237
4238 return diff_cmd
4239
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004240def MatchingFileType(file_name, extensions):
4241 """Returns true if the file name ends with one of the given extensions."""
4242 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004243
enne@chromium.org555cfe42014-01-29 18:21:39 +00004244@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004245def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004246 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00004247 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004248 GN_EXTS = ['.gn', '.gni']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004249 parser.add_option('--full', action='store_true',
4250 help='Reformat the full content of all touched files')
4251 parser.add_option('--dry-run', action='store_true',
4252 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004253 parser.add_option('--python', action='store_true',
4254 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004255 parser.add_option('--diff', action='store_true',
4256 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004257 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004258
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004259 # git diff generates paths against the root of the repository. Change
4260 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004261 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004262 if rel_base_path:
4263 os.chdir(rel_base_path)
4264
digit@chromium.org29e47272013-05-17 17:01:46 +00004265 # Grab the merge-base commit, i.e. the upstream commit of the current
4266 # branch when it was created or the last time it was rebased. This is
4267 # to cover the case where the user may have called "git fetch origin",
4268 # moving the origin branch to a newer commit, but hasn't rebased yet.
4269 upstream_commit = None
4270 cl = Changelist()
4271 upstream_branch = cl.GetUpstreamBranch()
4272 if upstream_branch:
4273 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4274 upstream_commit = upstream_commit.strip()
4275
4276 if not upstream_commit:
4277 DieWithError('Could not find base commit for this branch. '
4278 'Are you in detached state?')
4279
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004280 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4281 diff_output = RunGit(changed_files_cmd)
4282 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004283 # Filter out files deleted by this CL
4284 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004285
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004286 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4287 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4288 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004289 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004290
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004291 top_dir = os.path.normpath(
4292 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
4293
4294 # Locate the clang-format binary in the checkout
4295 try:
4296 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
4297 except clang_format.NotFoundError, e:
4298 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00004299
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004300 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4301 # formatted. This is used to block during the presubmit.
4302 return_value = 0
4303
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004304 if clang_diff_files:
4305 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004306 cmd = [clang_format_tool]
4307 if not opts.dry_run and not opts.diff:
4308 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004309 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004310 if opts.diff:
4311 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004312 else:
4313 env = os.environ.copy()
4314 env['PATH'] = str(os.path.dirname(clang_format_tool))
4315 try:
4316 script = clang_format.FindClangFormatScriptInChromiumTree(
4317 'clang-format-diff.py')
4318 except clang_format.NotFoundError, e:
4319 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004320
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004321 cmd = [sys.executable, script, '-p0']
4322 if not opts.dry_run and not opts.diff:
4323 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004324
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004325 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
4326 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004327
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004328 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
4329 if opts.diff:
4330 sys.stdout.write(stdout)
4331 if opts.dry_run and len(stdout) > 0:
4332 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004333
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004334 # Similar code to above, but using yapf on .py files rather than clang-format
4335 # on C/C++ files
4336 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004337 yapf_tool = gclient_utils.FindExecutable('yapf')
4338 if yapf_tool is None:
4339 DieWithError('yapf not found in PATH')
4340
4341 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004342 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004343 cmd = [yapf_tool]
4344 if not opts.dry_run and not opts.diff:
4345 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004346 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004347 if opts.diff:
4348 sys.stdout.write(stdout)
4349 else:
4350 # TODO(sbc): yapf --lines mode still has some issues.
4351 # https://github.com/google/yapf/issues/154
4352 DieWithError('--python currently only works with --full')
4353
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004354 # Dart's formatter does not have the nice property of only operating on
4355 # modified chunks, so hard code full.
4356 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004357 try:
4358 command = [dart_format.FindDartFmtToolInChromiumTree()]
4359 if not opts.dry_run and not opts.diff:
4360 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004361 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004362
ppi@chromium.org6593d932016-03-03 15:41:15 +00004363 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004364 if opts.dry_run and stdout:
4365 return_value = 2
4366 except dart_format.NotFoundError as e:
erikcorry@chromium.org3e445022015-12-17 09:07:26 +00004367 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
4368 'found in this checkout. Files in other languages are still ' +
4369 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004370
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004371 # Format GN build files. Always run on full build files for canonical form.
4372 if gn_diff_files:
4373 cmd = ['gn', 'format']
4374 if not opts.dry_run and not opts.diff:
4375 cmd.append('--in-place')
4376 for gn_diff_file in gn_diff_files:
4377 stdout = RunCommand(cmd + [gn_diff_file], cwd=top_dir)
4378 if opts.diff:
4379 sys.stdout.write(stdout)
4380
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004381 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004382
4383
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004384@subcommand.usage('<codereview url or issue id>')
4385def CMDcheckout(parser, args):
4386 """Checks out a branch associated with a given Rietveld issue."""
4387 _, args = parser.parse_args(args)
4388
4389 if len(args) != 1:
4390 parser.print_help()
4391 return 1
4392
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004393 target_issue = ParseIssueNum(args[0])
4394 if target_issue == None:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004395 parser.print_help()
4396 return 1
4397
4398 key_and_issues = [x.split() for x in RunGit(
4399 ['config', '--local', '--get-regexp', r'branch\..*\.rietveldissue'])
4400 .splitlines()]
4401 branches = []
4402 for key, issue in key_and_issues:
4403 if issue == target_issue:
4404 branches.append(re.sub(r'branch\.(.*)\.rietveldissue', r'\1', key))
4405
4406 if len(branches) == 0:
4407 print 'No branch found for issue %s.' % target_issue
4408 return 1
4409 if len(branches) == 1:
4410 RunGit(['checkout', branches[0]])
4411 else:
4412 print 'Multiple branches match issue %s:' % target_issue
4413 for i in range(len(branches)):
4414 print '%d: %s' % (i, branches[i])
4415 which = raw_input('Choose by index: ')
4416 try:
4417 RunGit(['checkout', branches[int(which)]])
4418 except (IndexError, ValueError):
4419 print 'Invalid selection, not checking out any branch.'
4420 return 1
4421
4422 return 0
4423
4424
maruel@chromium.org29404b52014-09-08 22:58:00 +00004425def CMDlol(parser, args):
4426 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00004427 print zlib.decompress(base64.b64decode(
4428 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
4429 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
4430 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
4431 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00004432 return 0
4433
4434
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004435class OptionParser(optparse.OptionParser):
4436 """Creates the option parse and add --verbose support."""
4437 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004438 optparse.OptionParser.__init__(
4439 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004440 self.add_option(
4441 '-v', '--verbose', action='count', default=0,
4442 help='Use 2 times for more debugging info')
4443
4444 def parse_args(self, args=None, values=None):
4445 options, args = optparse.OptionParser.parse_args(self, args, values)
4446 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
4447 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
4448 return options, args
4449
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004450
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004451def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00004452 if sys.hexversion < 0x02060000:
4453 print >> sys.stderr, (
4454 '\nYour python version %s is unsupported, please upgrade.\n' %
4455 sys.version.split(' ', 1)[0])
4456 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004457
maruel@chromium.orgddd59412011-11-30 14:20:38 +00004458 # Reload settings.
4459 global settings
4460 settings = Settings()
4461
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004462 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004463 dispatcher = subcommand.CommandDispatcher(__name__)
4464 try:
4465 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00004466 except auth.AuthenticationError as e:
4467 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004468 except urllib2.HTTPError, e:
4469 if e.code != 500:
4470 raise
4471 DieWithError(
4472 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
4473 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00004474 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004475
4476
4477if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004478 # These affect sys.stdout so do it outside of main() to simplify mocks in
4479 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00004480 fix_encoding.fix_encoding()
iannucci@chromium.org0703ea22016-04-01 01:02:42 +00004481 colorama.init(wrap="TERM" not in os.environ)
sbc@chromium.org013731e2015-02-26 18:28:43 +00004482 try:
4483 sys.exit(main(sys.argv[1:]))
4484 except KeyboardInterrupt:
4485 sys.stderr.write('interrupted\n')
4486 sys.exit(1)