blob: 6c78d997acbb6b8e2fb099a8330f134b6ef46a14 [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
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000048import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000049import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000050import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000051import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000052import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000053import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000054import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000055import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000056import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000057import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000058import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000059import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000060import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000061import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000062import watchlists
63
maruel@chromium.org0633fb42013-08-16 20:06:14 +000064__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000065
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000066DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000067POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000069GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
rmistry@google.comc68112d2015-03-03 12:48:06 +000070REFS_THAT_ALIAS_TO_OTHER_REFS = {
71 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
72 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
73}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000074
thestig@chromium.org44202a22014-03-11 19:22:18 +000075# Valid extensions for files we want to lint.
76DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
77DEFAULT_LINT_IGNORE_REGEX = r"$^"
78
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000079# Shortcut since it quickly becomes redundant.
80Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000081
maruel@chromium.orgddd59412011-11-30 14:20:38 +000082# Initialized in main()
83settings = None
84
85
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000086def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000087 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000088 sys.exit(1)
89
90
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000091def GetNoGitPagerEnv():
92 env = os.environ.copy()
93 # 'cat' is a magical git string that disables pagers on all platforms.
94 env['GIT_PAGER'] = 'cat'
95 return env
96
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000097
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000098def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000099 try:
maruel@chromium.org373af802012-05-25 21:07:33 +0000100 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000101 except subprocess2.CalledProcessError as e:
102 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000103 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000104 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000105 'Command "%s" failed.\n%s' % (
106 ' '.join(args), error_message or e.stdout or ''))
107 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000108
109
110def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000111 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000112 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000113
114
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000115def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000116 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000117 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000118 if suppress_stderr:
119 stderr = subprocess2.VOID
120 else:
121 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000122 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000123 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000124 stdout=subprocess2.PIPE,
125 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000126 return code, out[0]
127 except ValueError:
128 # When the subprocess fails, it returns None. That triggers a ValueError
129 # when trying to unpack the return value into (out, code).
130 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000131
132
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000133def RunGitSilent(args):
134 """Returns stdout, suppresses stderr and ingores the return code."""
135 return RunGitWithCode(args, suppress_stderr=True)[1]
136
137
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000138def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000139 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000140 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000141 return (version.startswith(prefix) and
142 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000143
144
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000145def BranchExists(branch):
146 """Return True if specified branch exists."""
147 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
148 suppress_stderr=True)
149 return not code
150
151
maruel@chromium.org90541732011-04-01 17:54:18 +0000152def ask_for_data(prompt):
153 try:
154 return raw_input(prompt)
155 except KeyboardInterrupt:
156 # Hide the exception.
157 sys.exit(1)
158
159
iannucci@chromium.org79540052012-10-19 23:15:26 +0000160def git_set_branch_value(key, value):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000161 branch = GetCurrentBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000162 if not branch:
163 return
164
165 cmd = ['config']
166 if isinstance(value, int):
167 cmd.append('--int')
168 git_key = 'branch.%s.%s' % (branch, key)
169 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000170
171
172def git_get_branch_default(key, default):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000173 branch = GetCurrentBranch()
iannucci@chromium.org79540052012-10-19 23:15:26 +0000174 if branch:
175 git_key = 'branch.%s.%s' % (branch, key)
176 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
177 try:
178 return int(stdout.strip())
179 except ValueError:
180 pass
181 return default
182
183
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000184def add_git_similarity(parser):
185 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000186 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000187 help='Sets the percentage that a pair of files need to match in order to'
188 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000189 parser.add_option(
190 '--find-copies', action='store_true',
191 help='Allows git to look for copies.')
192 parser.add_option(
193 '--no-find-copies', action='store_false', dest='find_copies',
194 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000195
196 old_parser_args = parser.parse_args
197 def Parse(args):
198 options, args = old_parser_args(args)
199
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000200 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000201 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000202 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000203 print('Note: Saving similarity of %d%% in git config.'
204 % options.similarity)
205 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000206
iannucci@chromium.org79540052012-10-19 23:15:26 +0000207 options.similarity = max(0, min(options.similarity, 100))
208
209 if options.find_copies is None:
210 options.find_copies = bool(
211 git_get_branch_default('git-find-copies', True))
212 else:
213 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000214
215 print('Using %d%% similarity for rename/copy detection. '
216 'Override with --similarity.' % options.similarity)
217
218 return options, args
219 parser.parse_args = Parse
220
221
machenbach@chromium.org45453142015-09-15 08:45:22 +0000222def _get_properties_from_options(options):
223 properties = dict(x.split('=', 1) for x in options.properties)
224 for key, val in properties.iteritems():
225 try:
226 properties[key] = json.loads(val)
227 except ValueError:
228 pass # If a value couldn't be evaluated, treat it as a string.
229 return properties
230
231
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000232def _prefix_master(master):
233 """Convert user-specified master name to full master name.
234
235 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
236 name, while the developers always use shortened master name
237 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
238 function does the conversion for buildbucket migration.
239 """
240 prefix = 'master.'
241 if master.startswith(prefix):
242 return master
243 return '%s%s' % (prefix, master)
244
245
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000246def _buildbucket_retry(operation_name, http, *args, **kwargs):
247 """Retries requests to buildbucket service and returns parsed json content."""
248 try_count = 0
249 while True:
250 response, content = http.request(*args, **kwargs)
251 try:
252 content_json = json.loads(content)
253 except ValueError:
254 content_json = None
255
256 # Buildbucket could return an error even if status==200.
257 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000258 error = content_json.get('error')
259 if error.get('code') == 403:
260 raise BuildbucketResponseException(
261 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000262 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000263 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000264 raise BuildbucketResponseException(msg)
265
266 if response.status == 200:
267 if not content_json:
268 raise BuildbucketResponseException(
269 'Buildbucket returns invalid json content: %s.\n'
270 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
271 content)
272 return content_json
273 if response.status < 500 or try_count >= 2:
274 raise httplib2.HttpLib2Error(content)
275
276 # status >= 500 means transient failures.
277 logging.debug('Transient errors when %s. Will retry.', operation_name)
278 time.sleep(0.5 + 1.5*try_count)
279 try_count += 1
280 assert False, 'unreachable'
281
282
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000283def trigger_luci_job(changelist, masters, options):
284 """Send a job to run on LUCI."""
285 issue_props = changelist.GetIssueProperties()
286 issue = changelist.GetIssue()
287 patchset = changelist.GetMostRecentPatchset()
288 for builders_and_tests in sorted(masters.itervalues()):
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000289 # TODO(hinoka et al): add support for other properties.
290 # Currently, this completely ignores testfilter and other properties.
291 for builder in sorted(builders_and_tests):
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000292 luci_trigger.trigger(
293 builder, 'HEAD', issue, patchset, issue_props['project'])
294
295
machenbach@chromium.org45453142015-09-15 08:45:22 +0000296def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000297 rietveld_url = settings.GetDefaultServerUrl()
298 rietveld_host = urlparse.urlparse(rietveld_url).hostname
299 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
300 http = authenticator.authorize(httplib2.Http())
301 http.force_exception_to_status_code = True
302 issue_props = changelist.GetIssueProperties()
303 issue = changelist.GetIssue()
304 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000305 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000306
307 buildbucket_put_url = (
308 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000309 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000310 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
311 hostname=rietveld_host,
312 issue=issue,
313 patch=patchset)
314
315 batch_req_body = {'builds': []}
316 print_text = []
317 print_text.append('Tried jobs on:')
318 for master, builders_and_tests in sorted(masters.iteritems()):
319 print_text.append('Master: %s' % master)
320 bucket = _prefix_master(master)
321 for builder, tests in sorted(builders_and_tests.iteritems()):
322 print_text.append(' %s: %s' % (builder, tests))
323 parameters = {
324 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000325 'changes': [{
326 'author': {'email': issue_props['owner_email']},
327 'revision': options.revision,
328 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000329 'properties': {
330 'category': category,
331 'issue': issue,
332 'master': master,
333 'patch_project': issue_props['project'],
334 'patch_storage': 'rietveld',
335 'patchset': patchset,
336 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000337 'rietveld': rietveld_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000338 },
339 }
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000340 if tests:
341 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000342 if properties:
343 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000344 if options.clobber:
345 parameters['properties']['clobber'] = True
346 batch_req_body['builds'].append(
347 {
348 'bucket': bucket,
349 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000350 'client_operation_id': str(uuid.uuid4()),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000351 'tags': ['builder:%s' % builder,
352 'buildset:%s' % buildset,
353 'master:%s' % master,
354 'user_agent:git_cl_try']
355 }
356 )
357
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000358 _buildbucket_retry(
359 'triggering tryjobs',
360 http,
361 buildbucket_put_url,
362 'PUT',
363 body=json.dumps(batch_req_body),
364 headers={'Content-Type': 'application/json'}
365 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000366 print_text.append('To see results here, run: git cl try-results')
367 print_text.append('To see results in browser, run: git cl web')
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000368 print '\n'.join(print_text)
kjellander@chromium.org44424542015-06-02 18:35:29 +0000369
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000370
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000371def fetch_try_jobs(auth_config, changelist, options):
372 """Fetches tryjobs from buildbucket.
373
374 Returns a map from build id to build info as json dictionary.
375 """
376 rietveld_url = settings.GetDefaultServerUrl()
377 rietveld_host = urlparse.urlparse(rietveld_url).hostname
378 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
379 if authenticator.has_cached_credentials():
380 http = authenticator.authorize(httplib2.Http())
381 else:
382 print ('Warning: Some results might be missing because %s' %
383 # Get the message on how to login.
384 auth.LoginRequiredError(rietveld_host).message)
385 http = httplib2.Http()
386
387 http.force_exception_to_status_code = True
388
389 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
390 hostname=rietveld_host,
391 issue=changelist.GetIssue(),
392 patch=options.patchset)
393 params = {'tag': 'buildset:%s' % buildset}
394
395 builds = {}
396 while True:
397 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
398 hostname=options.buildbucket_host,
399 params=urllib.urlencode(params))
400 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
401 for build in content.get('builds', []):
402 builds[build['id']] = build
403 if 'next_cursor' in content:
404 params['start_cursor'] = content['next_cursor']
405 else:
406 break
407 return builds
408
409
410def print_tryjobs(options, builds):
411 """Prints nicely result of fetch_try_jobs."""
412 if not builds:
413 print 'No tryjobs scheduled'
414 return
415
416 # Make a copy, because we'll be modifying builds dictionary.
417 builds = builds.copy()
418 builder_names_cache = {}
419
420 def get_builder(b):
421 try:
422 return builder_names_cache[b['id']]
423 except KeyError:
424 try:
425 parameters = json.loads(b['parameters_json'])
426 name = parameters['builder_name']
427 except (ValueError, KeyError) as error:
428 print 'WARNING: failed to get builder name for build %s: %s' % (
429 b['id'], error)
430 name = None
431 builder_names_cache[b['id']] = name
432 return name
433
434 def get_bucket(b):
435 bucket = b['bucket']
436 if bucket.startswith('master.'):
437 return bucket[len('master.'):]
438 return bucket
439
440 if options.print_master:
441 name_fmt = '%%-%ds %%-%ds' % (
442 max(len(str(get_bucket(b))) for b in builds.itervalues()),
443 max(len(str(get_builder(b))) for b in builds.itervalues()))
444 def get_name(b):
445 return name_fmt % (get_bucket(b), get_builder(b))
446 else:
447 name_fmt = '%%-%ds' % (
448 max(len(str(get_builder(b))) for b in builds.itervalues()))
449 def get_name(b):
450 return name_fmt % get_builder(b)
451
452 def sort_key(b):
453 return b['status'], b.get('result'), get_name(b), b.get('url')
454
455 def pop(title, f, color=None, **kwargs):
456 """Pop matching builds from `builds` dict and print them."""
457
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000458 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000459 colorize = str
460 else:
461 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
462
463 result = []
464 for b in builds.values():
465 if all(b.get(k) == v for k, v in kwargs.iteritems()):
466 builds.pop(b['id'])
467 result.append(b)
468 if result:
469 print colorize(title)
470 for b in sorted(result, key=sort_key):
471 print ' ', colorize('\t'.join(map(str, f(b))))
472
473 total = len(builds)
474 pop(status='COMPLETED', result='SUCCESS',
475 title='Successes:', color=Fore.GREEN,
476 f=lambda b: (get_name(b), b.get('url')))
477 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
478 title='Infra Failures:', color=Fore.MAGENTA,
479 f=lambda b: (get_name(b), b.get('url')))
480 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
481 title='Failures:', color=Fore.RED,
482 f=lambda b: (get_name(b), b.get('url')))
483 pop(status='COMPLETED', result='CANCELED',
484 title='Canceled:', color=Fore.MAGENTA,
485 f=lambda b: (get_name(b),))
486 pop(status='COMPLETED', result='FAILURE',
487 failure_reason='INVALID_BUILD_DEFINITION',
488 title='Wrong master/builder name:', color=Fore.MAGENTA,
489 f=lambda b: (get_name(b),))
490 pop(status='COMPLETED', result='FAILURE',
491 title='Other failures:',
492 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
493 pop(status='COMPLETED',
494 title='Other finished:',
495 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
496 pop(status='STARTED',
497 title='Started:', color=Fore.YELLOW,
498 f=lambda b: (get_name(b), b.get('url')))
499 pop(status='SCHEDULED',
500 title='Scheduled:',
501 f=lambda b: (get_name(b), 'id=%s' % b['id']))
502 # The last section is just in case buildbucket API changes OR there is a bug.
503 pop(title='Other:',
504 f=lambda b: (get_name(b), 'id=%s' % b['id']))
505 assert len(builds) == 0
506 print 'Total: %d tryjobs' % total
507
508
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000509def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
510 """Return the corresponding git ref if |base_url| together with |glob_spec|
511 matches the full |url|.
512
513 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
514 """
515 fetch_suburl, as_ref = glob_spec.split(':')
516 if allow_wildcards:
517 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
518 if glob_match:
519 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
520 # "branches/{472,597,648}/src:refs/remotes/svn/*".
521 branch_re = re.escape(base_url)
522 if glob_match.group(1):
523 branch_re += '/' + re.escape(glob_match.group(1))
524 wildcard = glob_match.group(2)
525 if wildcard == '*':
526 branch_re += '([^/]*)'
527 else:
528 # Escape and replace surrounding braces with parentheses and commas
529 # with pipe symbols.
530 wildcard = re.escape(wildcard)
531 wildcard = re.sub('^\\\\{', '(', wildcard)
532 wildcard = re.sub('\\\\,', '|', wildcard)
533 wildcard = re.sub('\\\\}$', ')', wildcard)
534 branch_re += wildcard
535 if glob_match.group(3):
536 branch_re += re.escape(glob_match.group(3))
537 match = re.match(branch_re, url)
538 if match:
539 return re.sub('\*$', match.group(1), as_ref)
540
541 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
542 if fetch_suburl:
543 full_url = base_url + '/' + fetch_suburl
544 else:
545 full_url = base_url
546 if full_url == url:
547 return as_ref
548 return None
549
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000550
iannucci@chromium.org79540052012-10-19 23:15:26 +0000551def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000552 """Prints statistics about the change to the user."""
553 # --no-ext-diff is broken in some versions of Git, so try to work around
554 # this by overriding the environment (but there is still a problem if the
555 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000556 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000557 if 'GIT_EXTERNAL_DIFF' in env:
558 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000559
560 if find_copies:
561 similarity_options = ['--find-copies-harder', '-l100000',
562 '-C%s' % similarity]
563 else:
564 similarity_options = ['-M%s' % similarity]
565
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000566 try:
567 stdout = sys.stdout.fileno()
568 except AttributeError:
569 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000570 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000571 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000572 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000573 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000574
575
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000576class BuildbucketResponseException(Exception):
577 pass
578
579
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000580class Settings(object):
581 def __init__(self):
582 self.default_server = None
583 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000584 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000585 self.is_git_svn = None
586 self.svn_branch = None
587 self.tree_status_url = None
588 self.viewvc_url = None
589 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000590 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000591 self.squash_gerrit_uploads = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000592 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000593 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000594 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000595 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000596
597 def LazyUpdateIfNeeded(self):
598 """Updates the settings from a codereview.settings file, if available."""
599 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000600 # The only value that actually changes the behavior is
601 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000602 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000603 error_ok=True
604 ).strip().lower()
605
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000606 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000607 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000608 LoadCodereviewSettingsFromFile(cr_settings_file)
609 self.updated = True
610
611 def GetDefaultServerUrl(self, error_ok=False):
612 if not self.default_server:
613 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000614 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000615 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000616 if error_ok:
617 return self.default_server
618 if not self.default_server:
619 error_message = ('Could not find settings file. You must configure '
620 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000621 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000622 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000623 return self.default_server
624
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000625 @staticmethod
626 def GetRelativeRoot():
627 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000628
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000629 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000630 if self.root is None:
631 self.root = os.path.abspath(self.GetRelativeRoot())
632 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000633
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000634 def GetGitMirror(self, remote='origin'):
635 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000636 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000637 if not os.path.isdir(local_url):
638 return None
639 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
640 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
641 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
642 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
643 if mirror.exists():
644 return mirror
645 return None
646
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000647 def GetIsGitSvn(self):
648 """Return true if this repo looks like it's using git-svn."""
649 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000650 if self.GetPendingRefPrefix():
651 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
652 self.is_git_svn = False
653 else:
654 # If you have any "svn-remote.*" config keys, we think you're using svn.
655 self.is_git_svn = RunGitWithCode(
656 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000657 return self.is_git_svn
658
659 def GetSVNBranch(self):
660 if self.svn_branch is None:
661 if not self.GetIsGitSvn():
662 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
663
664 # Try to figure out which remote branch we're based on.
665 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000666 # 1) iterate through our branch history and find the svn URL.
667 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000668
669 # regexp matching the git-svn line that contains the URL.
670 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
671
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000672 # We don't want to go through all of history, so read a line from the
673 # pipe at a time.
674 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000675 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000676 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
677 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000678 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000679 for line in proc.stdout:
680 match = git_svn_re.match(line)
681 if match:
682 url = match.group(1)
683 proc.stdout.close() # Cut pipe.
684 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000685
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000686 if url:
687 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
688 remotes = RunGit(['config', '--get-regexp',
689 r'^svn-remote\..*\.url']).splitlines()
690 for remote in remotes:
691 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000692 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000693 remote = match.group(1)
694 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000695 rewrite_root = RunGit(
696 ['config', 'svn-remote.%s.rewriteRoot' % remote],
697 error_ok=True).strip()
698 if rewrite_root:
699 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000700 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000701 ['config', 'svn-remote.%s.fetch' % remote],
702 error_ok=True).strip()
703 if fetch_spec:
704 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
705 if self.svn_branch:
706 break
707 branch_spec = RunGit(
708 ['config', 'svn-remote.%s.branches' % remote],
709 error_ok=True).strip()
710 if branch_spec:
711 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
712 if self.svn_branch:
713 break
714 tag_spec = RunGit(
715 ['config', 'svn-remote.%s.tags' % remote],
716 error_ok=True).strip()
717 if tag_spec:
718 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
719 if self.svn_branch:
720 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000721
722 if not self.svn_branch:
723 DieWithError('Can\'t guess svn branch -- try specifying it on the '
724 'command line')
725
726 return self.svn_branch
727
728 def GetTreeStatusUrl(self, error_ok=False):
729 if not self.tree_status_url:
730 error_message = ('You must configure your tree status URL by running '
731 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000732 self.tree_status_url = self._GetRietveldConfig(
733 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000734 return self.tree_status_url
735
736 def GetViewVCUrl(self):
737 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000738 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000739 return self.viewvc_url
740
rmistry@google.com90752582014-01-14 21:04:50 +0000741 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000742 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000743
rmistry@google.com78948ed2015-07-08 23:09:57 +0000744 def GetIsSkipDependencyUpload(self, branch_name):
745 """Returns true if specified branch should skip dep uploads."""
746 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
747 error_ok=True)
748
rmistry@google.com5626a922015-02-26 14:03:30 +0000749 def GetRunPostUploadHook(self):
750 run_post_upload_hook = self._GetRietveldConfig(
751 'run-post-upload-hook', error_ok=True)
752 return run_post_upload_hook == "True"
753
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000754 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000755 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000756
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000757 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000758 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000759
ukai@chromium.orge8077812012-02-03 03:41:46 +0000760 def GetIsGerrit(self):
761 """Return true if this repo is assosiated with gerrit code review system."""
762 if self.is_gerrit is None:
763 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
764 return self.is_gerrit
765
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000766 def GetSquashGerritUploads(self):
767 """Return true if uploads to Gerrit should be squashed by default."""
768 if self.squash_gerrit_uploads is None:
769 self.squash_gerrit_uploads = (
770 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
771 error_ok=True).strip() == 'true')
772 return self.squash_gerrit_uploads
773
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000774 def GetGitEditor(self):
775 """Return the editor specified in the git config, or None if none is."""
776 if self.git_editor is None:
777 self.git_editor = self._GetConfig('core.editor', error_ok=True)
778 return self.git_editor or None
779
thestig@chromium.org44202a22014-03-11 19:22:18 +0000780 def GetLintRegex(self):
781 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
782 DEFAULT_LINT_REGEX)
783
784 def GetLintIgnoreRegex(self):
785 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
786 DEFAULT_LINT_IGNORE_REGEX)
787
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000788 def GetProject(self):
789 if not self.project:
790 self.project = self._GetRietveldConfig('project', error_ok=True)
791 return self.project
792
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000793 def GetForceHttpsCommitUrl(self):
794 if not self.force_https_commit_url:
795 self.force_https_commit_url = self._GetRietveldConfig(
796 'force-https-commit-url', error_ok=True)
797 return self.force_https_commit_url
798
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000799 def GetPendingRefPrefix(self):
800 if not self.pending_ref_prefix:
801 self.pending_ref_prefix = self._GetRietveldConfig(
802 'pending-ref-prefix', error_ok=True)
803 return self.pending_ref_prefix
804
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000805 def _GetRietveldConfig(self, param, **kwargs):
806 return self._GetConfig('rietveld.' + param, **kwargs)
807
rmistry@google.com78948ed2015-07-08 23:09:57 +0000808 def _GetBranchConfig(self, branch_name, param, **kwargs):
809 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
810
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000811 def _GetConfig(self, param, **kwargs):
812 self.LazyUpdateIfNeeded()
813 return RunGit(['config', param], **kwargs).strip()
814
815
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000816def ShortBranchName(branch):
817 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000818 return branch.replace('refs/heads/', '', 1)
819
820
821def GetCurrentBranchRef():
822 """Returns branch ref (e.g., refs/heads/master) or None."""
823 return RunGit(['symbolic-ref', 'HEAD'],
824 stderr=subprocess2.VOID, error_ok=True).strip() or None
825
826
827def GetCurrentBranch():
828 """Returns current branch or None.
829
830 For refs/heads/* branches, returns just last part. For others, full ref.
831 """
832 branchref = GetCurrentBranchRef()
833 if branchref:
834 return ShortBranchName(branchref)
835 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000836
837
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000838class _ParsedIssueNumberArgument(object):
839 def __init__(self, issue=None, patchset=None, hostname=None):
840 self.issue = issue
841 self.patchset = patchset
842 self.hostname = hostname
843
844 @property
845 def valid(self):
846 return self.issue is not None
847
848
849class _RietveldParsedIssueNumberArgument(_ParsedIssueNumberArgument):
850 def __init__(self, *args, **kwargs):
851 self.patch_url = kwargs.pop('patch_url', None)
852 super(_RietveldParsedIssueNumberArgument, self).__init__(*args, **kwargs)
853
854
855def ParseIssueNumberArgument(arg):
856 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
857 fail_result = _ParsedIssueNumberArgument()
858
859 if arg.isdigit():
860 return _ParsedIssueNumberArgument(issue=int(arg))
861 if not arg.startswith('http'):
862 return fail_result
863 url = gclient_utils.UpgradeToHttps(arg)
864 try:
865 parsed_url = urlparse.urlparse(url)
866 except ValueError:
867 return fail_result
868 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
869 tmp = cls.ParseIssueURL(parsed_url)
870 if tmp is not None:
871 return tmp
872 return fail_result
873
874
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000875class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000876 """Changelist works with one changelist in local branch.
877
878 Supports two codereview backends: Rietveld or Gerrit, selected at object
879 creation.
880
881 Not safe for concurrent multi-{thread,process} use.
882 """
883
884 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
885 """Create a new ChangeList instance.
886
887 If issue is given, the codereview must be given too.
888
889 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
890 Otherwise, it's decided based on current configuration of the local branch,
891 with default being 'rietveld' for backwards compatibility.
892 See _load_codereview_impl for more details.
893
894 **kwargs will be passed directly to codereview implementation.
895 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000896 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000897 global settings
898 if not settings:
899 # Happens when git_cl.py is used as a utility library.
900 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000901
902 if issue:
903 assert codereview, 'codereview must be known, if issue is known'
904
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000905 self.branchref = branchref
906 if self.branchref:
907 self.branch = ShortBranchName(self.branchref)
908 else:
909 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000910 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000911 self.lookedup_issue = False
912 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000913 self.has_description = False
914 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000915 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000916 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000917 self.cc = None
918 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000919 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000920
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000921 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000922 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000923 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000924 assert self._codereview_impl
925 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000926
927 def _load_codereview_impl(self, codereview=None, **kwargs):
928 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000929 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
930 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
931 self._codereview = codereview
932 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000933 return
934
935 # Automatic selection based on issue number set for a current branch.
936 # Rietveld takes precedence over Gerrit.
937 assert not self.issue
938 # Whether we find issue or not, we are doing the lookup.
939 self.lookedup_issue = True
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000940 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000941 setting = cls.IssueSetting(self.GetBranch())
942 issue = RunGit(['config', setting], error_ok=True).strip()
943 if issue:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000944 self._codereview = codereview
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000945 self._codereview_impl = cls(self, **kwargs)
946 self.issue = int(issue)
947 return
948
949 # No issue is set for this branch, so decide based on repo-wide settings.
950 return self._load_codereview_impl(
951 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
952 **kwargs)
953
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000954 def IsGerrit(self):
955 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000956
957 def GetCCList(self):
958 """Return the users cc'd on this CL.
959
960 Return is a string suitable for passing to gcl with the --cc flag.
961 """
962 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000963 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000964 more_cc = ','.join(self.watchers)
965 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
966 return self.cc
967
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000968 def GetCCListWithoutDefault(self):
969 """Return the users cc'd on this CL excluding default ones."""
970 if self.cc is None:
971 self.cc = ','.join(self.watchers)
972 return self.cc
973
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000974 def SetWatchers(self, watchers):
975 """Set the list of email addresses that should be cc'd based on the changed
976 files in this CL.
977 """
978 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000979
980 def GetBranch(self):
981 """Returns the short branch name, e.g. 'master'."""
982 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000983 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000984 if not branchref:
985 return None
986 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000987 self.branch = ShortBranchName(self.branchref)
988 return self.branch
989
990 def GetBranchRef(self):
991 """Returns the full branch name, e.g. 'refs/heads/master'."""
992 self.GetBranch() # Poke the lazy loader.
993 return self.branchref
994
tandrii@chromium.org534f67a2016-04-07 18:47:05 +0000995 def ClearBranch(self):
996 """Clears cached branch data of this object."""
997 self.branch = self.branchref = None
998
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000999 @staticmethod
1000 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001001 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001002 e.g. 'origin', 'refs/heads/master'
1003 """
1004 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001005 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
1006 error_ok=True).strip()
1007 if upstream_branch:
1008 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
1009 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001010 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1011 error_ok=True).strip()
1012 if upstream_branch:
1013 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001014 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001015 # Fall back on trying a git-svn upstream branch.
1016 if settings.GetIsGitSvn():
1017 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001018 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001019 # Else, try to guess the origin remote.
1020 remote_branches = RunGit(['branch', '-r']).split()
1021 if 'origin/master' in remote_branches:
1022 # Fall back on origin/master if it exits.
1023 remote = 'origin'
1024 upstream_branch = 'refs/heads/master'
1025 elif 'origin/trunk' in remote_branches:
1026 # Fall back on origin/trunk if it exists. Generally a shared
1027 # git-svn clone
1028 remote = 'origin'
1029 upstream_branch = 'refs/heads/trunk'
1030 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001031 DieWithError(
1032 'Unable to determine default branch to diff against.\n'
1033 'Either pass complete "git diff"-style arguments, like\n'
1034 ' git cl upload origin/master\n'
1035 'or verify this branch is set up to track another \n'
1036 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001037
1038 return remote, upstream_branch
1039
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001040 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001041 upstream_branch = self.GetUpstreamBranch()
1042 if not BranchExists(upstream_branch):
1043 DieWithError('The upstream for the current branch (%s) does not exist '
1044 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001045 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001046 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001047
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001048 def GetUpstreamBranch(self):
1049 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001050 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001051 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001052 upstream_branch = upstream_branch.replace('refs/heads/',
1053 'refs/remotes/%s/' % remote)
1054 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1055 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001056 self.upstream_branch = upstream_branch
1057 return self.upstream_branch
1058
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001059 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001060 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001061 remote, branch = None, self.GetBranch()
1062 seen_branches = set()
1063 while branch not in seen_branches:
1064 seen_branches.add(branch)
1065 remote, branch = self.FetchUpstreamTuple(branch)
1066 branch = ShortBranchName(branch)
1067 if remote != '.' or branch.startswith('refs/remotes'):
1068 break
1069 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001070 remotes = RunGit(['remote'], error_ok=True).split()
1071 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001072 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001073 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001074 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001075 logging.warning('Could not determine which remote this change is '
1076 'associated with, so defaulting to "%s". This may '
1077 'not be what you want. You may prevent this message '
1078 'by running "git svn info" as documented here: %s',
1079 self._remote,
1080 GIT_INSTRUCTIONS_URL)
1081 else:
1082 logging.warn('Could not determine which remote this change is '
1083 'associated with. You may prevent this message by '
1084 'running "git svn info" as documented here: %s',
1085 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001086 branch = 'HEAD'
1087 if branch.startswith('refs/remotes'):
1088 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001089 elif branch.startswith('refs/branch-heads/'):
1090 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001091 else:
1092 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001093 return self._remote
1094
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001095 def GitSanityChecks(self, upstream_git_obj):
1096 """Checks git repo status and ensures diff is from local commits."""
1097
sbc@chromium.org79706062015-01-14 21:18:12 +00001098 if upstream_git_obj is None:
1099 if self.GetBranch() is None:
1100 print >> sys.stderr, (
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00001101 'ERROR: unable to determine current branch (detached HEAD?)')
sbc@chromium.org79706062015-01-14 21:18:12 +00001102 else:
1103 print >> sys.stderr, (
1104 'ERROR: no upstream branch')
1105 return False
1106
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001107 # Verify the commit we're diffing against is in our current branch.
1108 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1109 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1110 if upstream_sha != common_ancestor:
1111 print >> sys.stderr, (
1112 'ERROR: %s is not in the current branch. You may need to rebase '
1113 'your tracking branch' % upstream_sha)
1114 return False
1115
1116 # List the commits inside the diff, and verify they are all local.
1117 commits_in_diff = RunGit(
1118 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1119 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1120 remote_branch = remote_branch.strip()
1121 if code != 0:
1122 _, remote_branch = self.GetRemoteBranch()
1123
1124 commits_in_remote = RunGit(
1125 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1126
1127 common_commits = set(commits_in_diff) & set(commits_in_remote)
1128 if common_commits:
1129 print >> sys.stderr, (
1130 'ERROR: Your diff contains %d commits already in %s.\n'
1131 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1132 'the diff. If you are using a custom git flow, you can override'
1133 ' the reference used for this check with "git config '
1134 'gitcl.remotebranch <git-ref>".' % (
1135 len(common_commits), remote_branch, upstream_git_obj))
1136 return False
1137 return True
1138
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001139 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001140 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001141
1142 Returns None if it is not set.
1143 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001144 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
1145 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001146
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001147 def GetGitSvnRemoteUrl(self):
1148 """Return the configured git-svn remote URL parsed from git svn info.
1149
1150 Returns None if it is not set.
1151 """
1152 # URL is dependent on the current directory.
1153 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1154 if data:
1155 keys = dict(line.split(': ', 1) for line in data.splitlines()
1156 if ': ' in line)
1157 return keys.get('URL', None)
1158 return None
1159
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001160 def GetRemoteUrl(self):
1161 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1162
1163 Returns None if there is no remote.
1164 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001165 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001166 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1167
1168 # If URL is pointing to a local directory, it is probably a git cache.
1169 if os.path.isdir(url):
1170 url = RunGit(['config', 'remote.%s.url' % remote],
1171 error_ok=True,
1172 cwd=url).strip()
1173 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001174
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001175 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001176 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001177 if self.issue is None and not self.lookedup_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001178 issue = RunGit(['config',
1179 self._codereview_impl.IssueSetting(self.GetBranch())],
1180 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001181 self.issue = int(issue) or None if issue else None
1182 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001183 return self.issue
1184
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001185 def GetIssueURL(self):
1186 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001187 issue = self.GetIssue()
1188 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001189 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001190 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001191
1192 def GetDescription(self, pretty=False):
1193 if not self.has_description:
1194 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001195 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001196 self.has_description = True
1197 if pretty:
1198 wrapper = textwrap.TextWrapper()
1199 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1200 return wrapper.fill(self.description)
1201 return self.description
1202
1203 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001204 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001205 if self.patchset is None and not self.lookedup_patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001206 patchset = RunGit(['config', self._codereview_impl.PatchsetSetting()],
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001207 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001208 self.patchset = int(patchset) or None if patchset else None
1209 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001210 return self.patchset
1211
1212 def SetPatchset(self, patchset):
1213 """Set this branch's patchset. If patchset=0, clears the patchset."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001214 patchset_setting = self._codereview_impl.PatchsetSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001215 if patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001216 RunGit(['config', patchset_setting, str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001217 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001218 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001219 RunGit(['config', '--unset', patchset_setting],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001220 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001221 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001222
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001223 def SetIssue(self, issue=None):
1224 """Set this branch's issue. If issue isn't given, clears the issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001225 issue_setting = self._codereview_impl.IssueSetting(self.GetBranch())
1226 codereview_setting = self._codereview_impl.GetCodereviewServerSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001227 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001228 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001229 RunGit(['config', issue_setting, str(issue)])
1230 codereview_server = self._codereview_impl.GetCodereviewServer()
1231 if codereview_server:
1232 RunGit(['config', codereview_setting, codereview_server])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001233 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +00001234 current_issue = self.GetIssue()
1235 if current_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001236 RunGit(['config', '--unset', issue_setting])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001237 self.issue = None
1238 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001239
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001240 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001241 if not self.GitSanityChecks(upstream_branch):
1242 DieWithError('\nGit sanity check failure')
1243
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001244 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001245 if not root:
1246 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001247 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001248
1249 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001250 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001251 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001252 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001253 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001254 except subprocess2.CalledProcessError:
1255 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001256 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001257 'This branch probably doesn\'t exist anymore. To reset the\n'
1258 'tracking branch, please run\n'
1259 ' git branch --set-upstream %s trunk\n'
1260 'replacing trunk with origin/master or the relevant branch') %
1261 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001262
maruel@chromium.org52424302012-08-29 15:14:30 +00001263 issue = self.GetIssue()
1264 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001265 if issue:
1266 description = self.GetDescription()
1267 else:
1268 # If the change was never uploaded, use the log messages of all commits
1269 # up to the branch point, as git cl upload will prefill the description
1270 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001271 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1272 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001273
1274 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001275 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001276 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001277 name,
1278 description,
1279 absroot,
1280 files,
1281 issue,
1282 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001283 author,
1284 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001285
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001286 def UpdateDescription(self, description):
1287 self.description = description
1288 return self._codereview_impl.UpdateDescriptionRemote(description)
1289
1290 def RunHook(self, committing, may_prompt, verbose, change):
1291 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1292 try:
1293 return presubmit_support.DoPresubmitChecks(change, committing,
1294 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1295 default_presubmit=None, may_prompt=may_prompt,
1296 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit())
1297 except presubmit_support.PresubmitFailure, e:
1298 DieWithError(
1299 ('%s\nMaybe your depot_tools is out of date?\n'
1300 'If all fails, contact maruel@') % e)
1301
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001302 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1303 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001304 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1305 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001306 else:
1307 # Assume url.
1308 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1309 urlparse.urlparse(issue_arg))
1310 if not parsed_issue_arg or not parsed_issue_arg.valid:
1311 DieWithError('Failed to parse issue argument "%s". '
1312 'Must be an issue number or a valid URL.' % issue_arg)
1313 return self._codereview_impl.CMDPatchWithParsedIssue(
1314 parsed_issue_arg, reject, nocommit, directory)
1315
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001316 # Forward methods to codereview specific implementation.
1317
1318 def CloseIssue(self):
1319 return self._codereview_impl.CloseIssue()
1320
1321 def GetStatus(self):
1322 return self._codereview_impl.GetStatus()
1323
1324 def GetCodereviewServer(self):
1325 return self._codereview_impl.GetCodereviewServer()
1326
1327 def GetApprovingReviewers(self):
1328 return self._codereview_impl.GetApprovingReviewers()
1329
1330 def GetMostRecentPatchset(self):
1331 return self._codereview_impl.GetMostRecentPatchset()
1332
1333 def __getattr__(self, attr):
1334 # This is because lots of untested code accesses Rietveld-specific stuff
1335 # directly, and it's hard to fix for sure. So, just let it work, and fix
1336 # on a cases by case basis.
1337 return getattr(self._codereview_impl, attr)
1338
1339
1340class _ChangelistCodereviewBase(object):
1341 """Abstract base class encapsulating codereview specifics of a changelist."""
1342 def __init__(self, changelist):
1343 self._changelist = changelist # instance of Changelist
1344
1345 def __getattr__(self, attr):
1346 # Forward methods to changelist.
1347 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1348 # _RietveldChangelistImpl to avoid this hack?
1349 return getattr(self._changelist, attr)
1350
1351 def GetStatus(self):
1352 """Apply a rough heuristic to give a simple summary of an issue's review
1353 or CQ status, assuming adherence to a common workflow.
1354
1355 Returns None if no issue for this branch, or specific string keywords.
1356 """
1357 raise NotImplementedError()
1358
1359 def GetCodereviewServer(self):
1360 """Returns server URL without end slash, like "https://codereview.com"."""
1361 raise NotImplementedError()
1362
1363 def FetchDescription(self):
1364 """Fetches and returns description from the codereview server."""
1365 raise NotImplementedError()
1366
1367 def GetCodereviewServerSetting(self):
1368 """Returns git config setting for the codereview server."""
1369 raise NotImplementedError()
1370
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001371 @classmethod
1372 def IssueSetting(cls, branch):
1373 return 'branch.%s.%s' % (branch, cls.IssueSettingPrefix())
1374
1375 @classmethod
1376 def IssueSettingPrefix(cls):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001377 """Returns name of git config setting which stores issue number for a given
1378 branch."""
1379 raise NotImplementedError()
1380
1381 def PatchsetSetting(self):
1382 """Returns name of git config setting which stores issue number."""
1383 raise NotImplementedError()
1384
1385 def GetRieveldObjForPresubmit(self):
1386 # This is an unfortunate Rietveld-embeddedness in presubmit.
1387 # For non-Rietveld codereviews, this probably should return a dummy object.
1388 raise NotImplementedError()
1389
1390 def UpdateDescriptionRemote(self, description):
1391 """Update the description on codereview site."""
1392 raise NotImplementedError()
1393
1394 def CloseIssue(self):
1395 """Closes the issue."""
1396 raise NotImplementedError()
1397
1398 def GetApprovingReviewers(self):
1399 """Returns a list of reviewers approving the change.
1400
1401 Note: not necessarily committers.
1402 """
1403 raise NotImplementedError()
1404
1405 def GetMostRecentPatchset(self):
1406 """Returns the most recent patchset number from the codereview site."""
1407 raise NotImplementedError()
1408
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001409 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1410 directory):
1411 """Fetches and applies the issue.
1412
1413 Arguments:
1414 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1415 reject: if True, reject the failed patch instead of switching to 3-way
1416 merge. Rietveld only.
1417 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1418 only.
1419 directory: switch to directory before applying the patch. Rietveld only.
1420 """
1421 raise NotImplementedError()
1422
1423 @staticmethod
1424 def ParseIssueURL(parsed_url):
1425 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1426 failed."""
1427 raise NotImplementedError()
1428
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001429 def CMDUploadChange(self, options, args, change):
1430 """Uploads a change to codereview."""
1431 raise NotImplementedError()
1432
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001433
1434class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1435 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1436 super(_RietveldChangelistImpl, self).__init__(changelist)
1437 assert settings, 'must be initialized in _ChangelistCodereviewBase'
1438 settings.GetDefaultServerUrl()
1439
1440 self._rietveld_server = rietveld_server
1441 self._auth_config = auth_config
1442 self._props = None
1443 self._rpc_server = None
1444
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001445 def GetCodereviewServer(self):
1446 if not self._rietveld_server:
1447 # If we're on a branch then get the server potentially associated
1448 # with that branch.
1449 if self.GetIssue():
1450 rietveld_server_setting = self.GetCodereviewServerSetting()
1451 if rietveld_server_setting:
1452 self._rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1453 ['config', rietveld_server_setting], error_ok=True).strip())
1454 if not self._rietveld_server:
1455 self._rietveld_server = settings.GetDefaultServerUrl()
1456 return self._rietveld_server
1457
1458 def FetchDescription(self):
1459 issue = self.GetIssue()
1460 assert issue
1461 try:
1462 return self.RpcServer().get_description(issue).strip()
1463 except urllib2.HTTPError as e:
1464 if e.code == 404:
1465 DieWithError(
1466 ('\nWhile fetching the description for issue %d, received a '
1467 '404 (not found)\n'
1468 'error. It is likely that you deleted this '
1469 'issue on the server. If this is the\n'
1470 'case, please run\n\n'
1471 ' git cl issue 0\n\n'
1472 'to clear the association with the deleted issue. Then run '
1473 'this command again.') % issue)
1474 else:
1475 DieWithError(
1476 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1477 except urllib2.URLError as e:
1478 print >> sys.stderr, (
1479 'Warning: Failed to retrieve CL description due to network '
1480 'failure.')
1481 return ''
1482
1483 def GetMostRecentPatchset(self):
1484 return self.GetIssueProperties()['patchsets'][-1]
1485
1486 def GetPatchSetDiff(self, issue, patchset):
1487 return self.RpcServer().get(
1488 '/download/issue%s_%s.diff' % (issue, patchset))
1489
1490 def GetIssueProperties(self):
1491 if self._props is None:
1492 issue = self.GetIssue()
1493 if not issue:
1494 self._props = {}
1495 else:
1496 self._props = self.RpcServer().get_issue_properties(issue, True)
1497 return self._props
1498
1499 def GetApprovingReviewers(self):
1500 return get_approving_reviewers(self.GetIssueProperties())
1501
1502 def AddComment(self, message):
1503 return self.RpcServer().add_comment(self.GetIssue(), message)
1504
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001505 def GetStatus(self):
1506 """Apply a rough heuristic to give a simple summary of an issue's review
1507 or CQ status, assuming adherence to a common workflow.
1508
1509 Returns None if no issue for this branch, or one of the following keywords:
1510 * 'error' - error from review tool (including deleted issues)
1511 * 'unsent' - not sent for review
1512 * 'waiting' - waiting for review
1513 * 'reply' - waiting for owner to reply to review
1514 * 'lgtm' - LGTM from at least one approved reviewer
1515 * 'commit' - in the commit queue
1516 * 'closed' - closed
1517 """
1518 if not self.GetIssue():
1519 return None
1520
1521 try:
1522 props = self.GetIssueProperties()
1523 except urllib2.HTTPError:
1524 return 'error'
1525
1526 if props.get('closed'):
1527 # Issue is closed.
1528 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001529 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001530 # Issue is in the commit queue.
1531 return 'commit'
1532
1533 try:
1534 reviewers = self.GetApprovingReviewers()
1535 except urllib2.HTTPError:
1536 return 'error'
1537
1538 if reviewers:
1539 # Was LGTM'ed.
1540 return 'lgtm'
1541
1542 messages = props.get('messages') or []
1543
1544 if not messages:
1545 # No message was sent.
1546 return 'unsent'
1547 if messages[-1]['sender'] != props.get('owner_email'):
1548 # Non-LGTM reply from non-owner
1549 return 'reply'
1550 return 'waiting'
1551
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001552 def UpdateDescriptionRemote(self, description):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001553 return self.RpcServer().update_description(
1554 self.GetIssue(), self.description)
1555
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001556 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001557 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001558
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001559 def SetFlag(self, flag, value):
1560 """Patchset must match."""
1561 if not self.GetPatchset():
1562 DieWithError('The patchset needs to match. Send another patchset.')
1563 try:
1564 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001565 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001566 except urllib2.HTTPError, e:
1567 if e.code == 404:
1568 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1569 if e.code == 403:
1570 DieWithError(
1571 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1572 'match?') % (self.GetIssue(), self.GetPatchset()))
1573 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001574
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001575 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001576 """Returns an upload.RpcServer() to access this review's rietveld instance.
1577 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001578 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001579 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001580 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001581 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001582 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001583
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001584 @classmethod
1585 def IssueSettingPrefix(cls):
1586 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001587
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001588 def PatchsetSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001589 """Return the git setting that stores this change's most recent patchset."""
1590 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1591
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001592 def GetCodereviewServerSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001593 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001594 branch = self.GetBranch()
1595 if branch:
1596 return 'branch.%s.rietveldserver' % branch
1597 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001598
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001599 def GetRieveldObjForPresubmit(self):
1600 return self.RpcServer()
1601
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001602 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1603 directory):
1604 # TODO(maruel): Use apply_issue.py
1605
1606 # PatchIssue should never be called with a dirty tree. It is up to the
1607 # caller to check this, but just in case we assert here since the
1608 # consequences of the caller not checking this could be dire.
1609 assert(not git_common.is_dirty_git_tree('apply'))
1610 assert(parsed_issue_arg.valid)
1611 self._changelist.issue = parsed_issue_arg.issue
1612 if parsed_issue_arg.hostname:
1613 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1614
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001615 if (isinstance(parsed_issue_arg, _RietveldParsedIssueNumberArgument) and
1616 parsed_issue_arg.patch_url):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001617 assert parsed_issue_arg.patchset
1618 patchset = parsed_issue_arg.patchset
1619 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1620 else:
1621 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1622 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1623
1624 # Switch up to the top-level directory, if necessary, in preparation for
1625 # applying the patch.
1626 top = settings.GetRelativeRoot()
1627 if top:
1628 os.chdir(top)
1629
1630 # Git patches have a/ at the beginning of source paths. We strip that out
1631 # with a sed script rather than the -p flag to patch so we can feed either
1632 # Git or svn-style patches into the same apply command.
1633 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1634 try:
1635 patch_data = subprocess2.check_output(
1636 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1637 except subprocess2.CalledProcessError:
1638 DieWithError('Git patch mungling failed.')
1639 logging.info(patch_data)
1640
1641 # We use "git apply" to apply the patch instead of "patch" so that we can
1642 # pick up file adds.
1643 # The --index flag means: also insert into the index (so we catch adds).
1644 cmd = ['git', 'apply', '--index', '-p0']
1645 if directory:
1646 cmd.extend(('--directory', directory))
1647 if reject:
1648 cmd.append('--reject')
1649 elif IsGitVersionAtLeast('1.7.12'):
1650 cmd.append('--3way')
1651 try:
1652 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
1653 stdin=patch_data, stdout=subprocess2.VOID)
1654 except subprocess2.CalledProcessError:
1655 print 'Failed to apply the patch'
1656 return 1
1657
1658 # If we had an issue, commit the current state and register the issue.
1659 if not nocommit:
1660 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
1661 'patch from issue %(i)s at patchset '
1662 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
1663 % {'i': self.GetIssue(), 'p': patchset})])
1664 self.SetIssue(self.GetIssue())
1665 self.SetPatchset(patchset)
1666 print "Committed patch locally."
1667 else:
1668 print "Patch applied to index."
1669 return 0
1670
1671 @staticmethod
1672 def ParseIssueURL(parsed_url):
1673 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1674 return None
1675 # Typical url: https://domain/<issue_number>[/[other]]
1676 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
1677 if match:
1678 return _RietveldParsedIssueNumberArgument(
1679 issue=int(match.group(1)),
1680 hostname=parsed_url.netloc)
1681 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
1682 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
1683 if match:
1684 return _RietveldParsedIssueNumberArgument(
1685 issue=int(match.group(1)),
1686 patchset=int(match.group(2)),
1687 hostname=parsed_url.netloc,
1688 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
1689 return None
1690
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001691 def CMDUploadChange(self, options, args, change):
1692 """Upload the patch to Rietveld."""
1693 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1694 upload_args.extend(['--server', self.GetCodereviewServer()])
1695 # TODO(tandrii): refactor this ugliness into _RietveldChangelistImpl.
1696 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
1697 if options.emulate_svn_auto_props:
1698 upload_args.append('--emulate_svn_auto_props')
1699
1700 change_desc = None
1701
1702 if options.email is not None:
1703 upload_args.extend(['--email', options.email])
1704
1705 if self.GetIssue():
1706 if options.title:
1707 upload_args.extend(['--title', options.title])
1708 if options.message:
1709 upload_args.extend(['--message', options.message])
1710 upload_args.extend(['--issue', str(self.GetIssue())])
1711 print ('This branch is associated with issue %s. '
1712 'Adding patch to that issue.' % self.GetIssue())
1713 else:
1714 if options.title:
1715 upload_args.extend(['--title', options.title])
1716 message = (options.title or options.message or
1717 CreateDescriptionFromLog(args))
1718 change_desc = ChangeDescription(message)
1719 if options.reviewers or options.tbr_owners:
1720 change_desc.update_reviewers(options.reviewers,
1721 options.tbr_owners,
1722 change)
1723 if not options.force:
1724 change_desc.prompt()
1725
1726 if not change_desc.description:
1727 print "Description is empty; aborting."
1728 return 1
1729
1730 upload_args.extend(['--message', change_desc.description])
1731 if change_desc.get_reviewers():
1732 upload_args.append('--reviewers=%s' % ','.join(
1733 change_desc.get_reviewers()))
1734 if options.send_mail:
1735 if not change_desc.get_reviewers():
1736 DieWithError("Must specify reviewers to send email.")
1737 upload_args.append('--send_mail')
1738
1739 # We check this before applying rietveld.private assuming that in
1740 # rietveld.cc only addresses which we can send private CLs to are listed
1741 # if rietveld.private is set, and so we should ignore rietveld.cc only
1742 # when --private is specified explicitly on the command line.
1743 if options.private:
1744 logging.warn('rietveld.cc is ignored since private flag is specified. '
1745 'You need to review and add them manually if necessary.')
1746 cc = self.GetCCListWithoutDefault()
1747 else:
1748 cc = self.GetCCList()
1749 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
1750 if cc:
1751 upload_args.extend(['--cc', cc])
1752
1753 if options.private or settings.GetDefaultPrivateFlag() == "True":
1754 upload_args.append('--private')
1755
1756 upload_args.extend(['--git_similarity', str(options.similarity)])
1757 if not options.find_copies:
1758 upload_args.extend(['--git_no_find_copies'])
1759
1760 # Include the upstream repo's URL in the change -- this is useful for
1761 # projects that have their source spread across multiple repos.
1762 remote_url = self.GetGitBaseUrlFromConfig()
1763 if not remote_url:
1764 if settings.GetIsGitSvn():
1765 remote_url = self.GetGitSvnRemoteUrl()
1766 else:
1767 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
1768 remote_url = '%s@%s' % (self.GetRemoteUrl(),
1769 self.GetUpstreamBranch().split('/')[-1])
1770 if remote_url:
1771 upload_args.extend(['--base_url', remote_url])
1772 remote, remote_branch = self.GetRemoteBranch()
1773 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
1774 settings.GetPendingRefPrefix())
1775 if target_ref:
1776 upload_args.extend(['--target_ref', target_ref])
1777
1778 # Look for dependent patchsets. See crbug.com/480453 for more details.
1779 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
1780 upstream_branch = ShortBranchName(upstream_branch)
1781 if remote is '.':
1782 # A local branch is being tracked.
1783 local_branch = ShortBranchName(upstream_branch)
1784 if settings.GetIsSkipDependencyUpload(local_branch):
1785 print
1786 print ('Skipping dependency patchset upload because git config '
1787 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
1788 print
1789 else:
1790 auth_config = auth.extract_auth_config_from_options(options)
1791 branch_cl = Changelist(branchref=local_branch,
1792 auth_config=auth_config)
1793 branch_cl_issue_url = branch_cl.GetIssueURL()
1794 branch_cl_issue = branch_cl.GetIssue()
1795 branch_cl_patchset = branch_cl.GetPatchset()
1796 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
1797 upload_args.extend(
1798 ['--depends_on_patchset', '%s:%s' % (
1799 branch_cl_issue, branch_cl_patchset)])
1800 print (
1801 '\n'
1802 'The current branch (%s) is tracking a local branch (%s) with '
1803 'an associated CL.\n'
1804 'Adding %s/#ps%s as a dependency patchset.\n'
1805 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
1806 branch_cl_patchset))
1807
1808 project = settings.GetProject()
1809 if project:
1810 upload_args.extend(['--project', project])
1811
1812 if options.cq_dry_run:
1813 upload_args.extend(['--cq_dry_run'])
1814
1815 try:
1816 upload_args = ['upload'] + upload_args + args
1817 logging.info('upload.RealMain(%s)', upload_args)
1818 issue, patchset = upload.RealMain(upload_args)
1819 issue = int(issue)
1820 patchset = int(patchset)
1821 except KeyboardInterrupt:
1822 sys.exit(1)
1823 except:
1824 # If we got an exception after the user typed a description for their
1825 # change, back up the description before re-raising.
1826 if change_desc:
1827 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1828 print('\nGot exception while uploading -- saving description to %s\n' %
1829 backup_path)
1830 backup_file = open(backup_path, 'w')
1831 backup_file.write(change_desc.description)
1832 backup_file.close()
1833 raise
1834
1835 if not self.GetIssue():
1836 self.SetIssue(issue)
1837 self.SetPatchset(patchset)
1838
1839 if options.use_commit_queue:
1840 self.SetFlag('commit', '1')
1841 return 0
1842
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001843
1844class _GerritChangelistImpl(_ChangelistCodereviewBase):
1845 def __init__(self, changelist, auth_config=None):
1846 # auth_config is Rietveld thing, kept here to preserve interface only.
1847 super(_GerritChangelistImpl, self).__init__(changelist)
1848 self._change_id = None
1849 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
1850 self._gerrit_host = None # e.g. chromium-review.googlesource.com
1851
1852 def _GetGerritHost(self):
1853 # Lazy load of configs.
1854 self.GetCodereviewServer()
1855 return self._gerrit_host
1856
1857 def GetCodereviewServer(self):
1858 if not self._gerrit_server:
1859 # If we're on a branch then get the server potentially associated
1860 # with that branch.
1861 if self.GetIssue():
1862 gerrit_server_setting = self.GetCodereviewServerSetting()
1863 if gerrit_server_setting:
1864 self._gerrit_server = RunGit(['config', gerrit_server_setting],
1865 error_ok=True).strip()
1866 if self._gerrit_server:
1867 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
1868 if not self._gerrit_server:
1869 # We assume repo to be hosted on Gerrit, and hence Gerrit server
1870 # has "-review" suffix for lowest level subdomain.
1871 parts = urlparse.urlparse(self.GetRemoteUrl()).netloc.split('.')
1872 parts[0] = parts[0] + '-review'
1873 self._gerrit_host = '.'.join(parts)
1874 self._gerrit_server = 'https://%s' % self._gerrit_host
1875 return self._gerrit_server
1876
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001877 @classmethod
1878 def IssueSettingPrefix(cls):
1879 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001880
1881 def PatchsetSetting(self):
1882 """Return the git setting that stores this change's most recent patchset."""
1883 return 'branch.%s.gerritpatchset' % self.GetBranch()
1884
1885 def GetCodereviewServerSetting(self):
1886 """Returns the git setting that stores this change's Gerrit server."""
1887 branch = self.GetBranch()
1888 if branch:
1889 return 'branch.%s.gerritserver' % branch
1890 return None
1891
1892 def GetRieveldObjForPresubmit(self):
1893 class ThisIsNotRietveldIssue(object):
1894 def __nonzero__(self):
1895 # This is a hack to make presubmit_support think that rietveld is not
1896 # defined, yet still ensure that calls directly result in a decent
1897 # exception message below.
1898 return False
1899
1900 def __getattr__(self, attr):
1901 print(
1902 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
1903 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
1904 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
1905 'or use Rietveld for codereview.\n'
1906 'See also http://crbug.com/579160.' % attr)
1907 raise NotImplementedError()
1908 return ThisIsNotRietveldIssue()
1909
1910 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001911 """Apply a rough heuristic to give a simple summary of an issue's review
1912 or CQ status, assuming adherence to a common workflow.
1913
1914 Returns None if no issue for this branch, or one of the following keywords:
1915 * 'error' - error from review tool (including deleted issues)
1916 * 'unsent' - no reviewers added
1917 * 'waiting' - waiting for review
1918 * 'reply' - waiting for owner to reply to review
1919 * 'not lgtm' - Code-Review -2 from at least one approved reviewer
1920 * 'lgtm' - Code-Review +2 from at least one approved reviewer
1921 * 'commit' - in the commit queue
1922 * 'closed' - abandoned
1923 """
1924 if not self.GetIssue():
1925 return None
1926
1927 try:
1928 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
1929 except httplib.HTTPException:
1930 return 'error'
1931
1932 if data['status'] == 'ABANDONED':
1933 return 'closed'
1934
1935 cq_label = data['labels'].get('Commit-Queue', {})
1936 if cq_label:
1937 # Vote value is a stringified integer, which we expect from 0 to 2.
1938 vote_value = cq_label.get('value', '0')
1939 vote_text = cq_label.get('values', {}).get(vote_value, '')
1940 if vote_text.lower() == 'commit':
1941 return 'commit'
1942
1943 lgtm_label = data['labels'].get('Code-Review', {})
1944 if lgtm_label:
1945 if 'rejected' in lgtm_label:
1946 return 'not lgtm'
1947 if 'approved' in lgtm_label:
1948 return 'lgtm'
1949
1950 if not data.get('reviewers', {}).get('REVIEWER', []):
1951 return 'unsent'
1952
1953 messages = data.get('messages', [])
1954 if messages:
1955 owner = data['owner'].get('_account_id')
1956 last_message_author = messages[-1].get('author', {}).get('_account_id')
1957 if owner != last_message_author:
1958 # Some reply from non-owner.
1959 return 'reply'
1960
1961 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001962
1963 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001964 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001965 return data['revisions'][data['current_revision']]['_number']
1966
1967 def FetchDescription(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001968 data = self._GetChangeDetail(['COMMIT_FOOTERS', 'CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001969 return data['revisions'][data['current_revision']]['commit_with_footers']
1970
1971 def UpdateDescriptionRemote(self, description):
1972 # TODO(tandrii)
1973 raise NotImplementedError()
1974
1975 def CloseIssue(self):
1976 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
1977
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001978 def SubmitIssue(self, wait_for_merge=True):
1979 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
1980 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001981
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001982 def _GetChangeDetail(self, options=None, issue=None):
1983 options = options or []
1984 issue = issue or self.GetIssue()
1985 assert issue, 'issue required to query Gerrit'
1986 return gerrit_util.GetChangeDetail(self._GetGerritHost(), options, issue)
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001987
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001988 def CMDLand(self, force, bypass_hooks, verbose):
1989 if git_common.is_dirty_git_tree('land'):
1990 return 1
1991 differs = True
1992 last_upload = RunGit(['config',
1993 'branch.%s.gerritsquashhash' % self.GetBranch()],
1994 error_ok=True).strip()
1995 # Note: git diff outputs nothing if there is no diff.
1996 if not last_upload or RunGit(['diff', last_upload]).strip():
1997 print('WARNING: some changes from local branch haven\'t been uploaded')
1998 else:
1999 detail = self._GetChangeDetail(['CURRENT_REVISION'])
2000 if detail['current_revision'] == last_upload:
2001 differs = False
2002 else:
2003 print('WARNING: local branch contents differ from latest uploaded '
2004 'patchset')
2005 if differs:
2006 if not force:
2007 ask_for_data(
2008 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2009 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2010 elif not bypass_hooks:
2011 hook_results = self.RunHook(
2012 committing=True,
2013 may_prompt=not force,
2014 verbose=verbose,
2015 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2016 if not hook_results.should_continue():
2017 return 1
2018
2019 self.SubmitIssue(wait_for_merge=True)
2020 print('Issue %s has been submitted.' % self.GetIssueURL())
2021 return 0
2022
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002023 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2024 directory):
2025 assert not reject
2026 assert not nocommit
2027 assert not directory
2028 assert parsed_issue_arg.valid
2029
2030 self._changelist.issue = parsed_issue_arg.issue
2031
2032 if parsed_issue_arg.hostname:
2033 self._gerrit_host = parsed_issue_arg.hostname
2034 self._gerrit_server = 'https://%s' % self._gerrit_host
2035
2036 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2037
2038 if not parsed_issue_arg.patchset:
2039 # Use current revision by default.
2040 revision_info = detail['revisions'][detail['current_revision']]
2041 patchset = int(revision_info['_number'])
2042 else:
2043 patchset = parsed_issue_arg.patchset
2044 for revision_info in detail['revisions'].itervalues():
2045 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2046 break
2047 else:
2048 DieWithError('Couldn\'t find patchset %i in issue %i' %
2049 (parsed_issue_arg.patchset, self.GetIssue()))
2050
2051 fetch_info = revision_info['fetch']['http']
2052 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2053 RunGit(['cherry-pick', 'FETCH_HEAD'])
2054 self.SetIssue(self.GetIssue())
2055 self.SetPatchset(patchset)
2056 print('Committed patch for issue %i pathset %i locally' %
2057 (self.GetIssue(), self.GetPatchset()))
2058 return 0
2059
2060 @staticmethod
2061 def ParseIssueURL(parsed_url):
2062 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2063 return None
2064 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2065 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2066 # Short urls like https://domain/<issue_number> can be used, but don't allow
2067 # specifying the patchset (you'd 404), but we allow that here.
2068 if parsed_url.path == '/':
2069 part = parsed_url.fragment
2070 else:
2071 part = parsed_url.path
2072 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2073 if match:
2074 return _ParsedIssueNumberArgument(
2075 issue=int(match.group(2)),
2076 patchset=int(match.group(4)) if match.group(4) else None,
2077 hostname=parsed_url.netloc)
2078 return None
2079
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002080 def CMDUploadChange(self, options, args, change):
2081 """Upload the current branch to Gerrit."""
2082 # We assume the remote called "origin" is the one we want.
2083 # It is probably not worthwhile to support different workflows.
2084 gerrit_remote = 'origin'
2085
2086 remote, remote_branch = self.GetRemoteBranch()
2087 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2088 pending_prefix='')
2089
2090 if options.title:
2091 # TODO(tandrii): it's now supported by Gerrit, implement!
2092 print "\nPatch titles (-t) are not supported in Gerrit. Aborting..."
2093 return 1
2094
2095 if options.squash:
2096 if not self.GetIssue():
2097 # TODO(tandrii): deperecate this after 2016Q2. Backwards compatibility
2098 # with shadow branch, which used to contain change-id for a given
2099 # branch, using which we can fetch actual issue number and set it as the
2100 # property of the branch, which is the new way.
2101 message = RunGitSilent([
2102 'show', '--format=%B', '-s',
2103 'refs/heads/git_cl_uploads/%s' % self.GetBranch()])
2104 if message:
2105 change_ids = git_footers.get_footer_change_id(message.strip())
2106 if change_ids and len(change_ids) == 1:
2107 details = self._GetChangeDetail(issue=change_ids[0])
2108 if details:
2109 print('WARNING: found old upload in branch git_cl_uploads/%s '
2110 'corresponding to issue %s' %
2111 (self.GetBranch(), details['_number']))
2112 self.SetIssue(details['_number'])
2113 if not self.GetIssue():
2114 DieWithError(
2115 '\n' # For readability of the blob below.
2116 'Found old upload in branch git_cl_uploads/%s, '
2117 'but failed to find corresponding Gerrit issue.\n'
2118 'If you know the issue number, set it manually first:\n'
2119 ' git cl issue 123456\n'
2120 'If you intended to upload this CL as new issue, '
2121 'just delete or rename the old upload branch:\n'
2122 ' git rename-branch git_cl_uploads/%s old_upload-%s\n'
2123 'After that, please run git cl upload again.' %
2124 tuple([self.GetBranch()] * 3))
2125 # End of backwards compatability.
2126
2127 if self.GetIssue():
2128 # Try to get the message from a previous upload.
2129 message = self.GetDescription()
2130 if not message:
2131 DieWithError(
2132 'failed to fetch description from current Gerrit issue %d\n'
2133 '%s' % (self.GetIssue(), self.GetIssueURL()))
2134 change_id = self._GetChangeDetail()['change_id']
2135 while True:
2136 footer_change_ids = git_footers.get_footer_change_id(message)
2137 if footer_change_ids == [change_id]:
2138 break
2139 if not footer_change_ids:
2140 message = git_footers.add_footer_change_id(message, change_id)
2141 print('WARNING: appended missing Change-Id to issue description')
2142 continue
2143 # There is already a valid footer but with different or several ids.
2144 # Doing this automatically is non-trivial as we don't want to lose
2145 # existing other footers, yet we want to append just 1 desired
2146 # Change-Id. Thus, just create a new footer, but let user verify the
2147 # new description.
2148 message = '%s\n\nChange-Id: %s' % (message, change_id)
2149 print(
2150 'WARNING: issue %s has Change-Id footer(s):\n'
2151 ' %s\n'
2152 'but issue has Change-Id %s, according to Gerrit.\n'
2153 'Please, check the proposed correction to the description, '
2154 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2155 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2156 change_id))
2157 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2158 if not options.force:
2159 change_desc = ChangeDescription(message)
2160 change_desc.prompt()
2161 message = change_desc.description
2162 if not message:
2163 DieWithError("Description is empty. Aborting...")
2164 # Continue the while loop.
2165 # Sanity check of this code - we should end up with proper message
2166 # footer.
2167 assert [change_id] == git_footers.get_footer_change_id(message)
2168 change_desc = ChangeDescription(message)
2169 else:
2170 change_desc = ChangeDescription(
2171 options.message or CreateDescriptionFromLog(args))
2172 if not options.force:
2173 change_desc.prompt()
2174 if not change_desc.description:
2175 DieWithError("Description is empty. Aborting...")
2176 message = change_desc.description
2177 change_ids = git_footers.get_footer_change_id(message)
2178 if len(change_ids) > 1:
2179 DieWithError('too many Change-Id footers, at most 1 allowed.')
2180 if not change_ids:
2181 # Generate the Change-Id automatically.
2182 message = git_footers.add_footer_change_id(
2183 message, GenerateGerritChangeId(message))
2184 change_desc.set_description(message)
2185 change_ids = git_footers.get_footer_change_id(message)
2186 assert len(change_ids) == 1
2187 change_id = change_ids[0]
2188
2189 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2190 if remote is '.':
2191 # If our upstream branch is local, we base our squashed commit on its
2192 # squashed version.
2193 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2194 # Check the squashed hash of the parent.
2195 parent = RunGit(['config',
2196 'branch.%s.gerritsquashhash' % upstream_branch_name],
2197 error_ok=True).strip()
2198 # Verify that the upstream branch has been uploaded too, otherwise
2199 # Gerrit will create additional CLs when uploading.
2200 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2201 RunGitSilent(['rev-parse', parent + ':'])):
2202 # TODO(tandrii): remove "old depot_tools" part on April 12, 2016.
2203 DieWithError(
2204 'Upload upstream branch %s first.\n'
2205 'Note: maybe you\'ve uploaded it with --no-squash or with an old '
2206 'version of depot_tools. If so, then re-upload it with:\n'
2207 ' git cl upload --squash\n' % upstream_branch_name)
2208 else:
2209 parent = self.GetCommonAncestorWithUpstream()
2210
2211 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2212 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2213 '-m', message]).strip()
2214 else:
2215 change_desc = ChangeDescription(
2216 options.message or CreateDescriptionFromLog(args))
2217 if not change_desc.description:
2218 DieWithError("Description is empty. Aborting...")
2219
2220 if not git_footers.get_footer_change_id(change_desc.description):
2221 DownloadGerritHook(False)
2222 change_desc.set_description(AddChangeIdToCommitMessage(options, args))
2223 ref_to_push = 'HEAD'
2224 parent = '%s/%s' % (gerrit_remote, branch)
2225 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2226
2227 assert change_desc
2228 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2229 ref_to_push)]).splitlines()
2230 if len(commits) > 1:
2231 print('WARNING: This will upload %d commits. Run the following command '
2232 'to see which commits will be uploaded: ' % len(commits))
2233 print('git log %s..%s' % (parent, ref_to_push))
2234 print('You can also use `git squash-branch` to squash these into a '
2235 'single commit.')
2236 ask_for_data('About to upload; enter to confirm.')
2237
2238 if options.reviewers or options.tbr_owners:
2239 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2240 change)
2241
2242 receive_options = []
2243 cc = self.GetCCList().split(',')
2244 if options.cc:
2245 cc.extend(options.cc)
2246 cc = filter(None, cc)
2247 if cc:
2248 receive_options += ['--cc=' + email for email in cc]
2249 if change_desc.get_reviewers():
2250 receive_options.extend(
2251 '--reviewer=' + email for email in change_desc.get_reviewers())
2252
2253 git_command = ['push']
2254 if receive_options:
2255 git_command.append('--receive-pack=git receive-pack %s' %
2256 ' '.join(receive_options))
2257 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
2258 push_stdout = gclient_utils.CheckCallAndFilter(
2259 ['git'] + git_command,
2260 print_stdout=True,
2261 # Flush after every line: useful for seeing progress when running as
2262 # recipe.
2263 filter_fn=lambda _: sys.stdout.flush())
2264
2265 if options.squash:
2266 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2267 change_numbers = [m.group(1)
2268 for m in map(regex.match, push_stdout.splitlines())
2269 if m]
2270 if len(change_numbers) != 1:
2271 DieWithError(
2272 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2273 'Change-Id: %s') % (len(change_numbers), change_id))
2274 self.SetIssue(change_numbers[0])
2275 RunGit(['config', 'branch.%s.gerritsquashhash' % self.GetBranch(),
2276 ref_to_push])
2277 return 0
2278
2279
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002280
2281_CODEREVIEW_IMPLEMENTATIONS = {
2282 'rietveld': _RietveldChangelistImpl,
2283 'gerrit': _GerritChangelistImpl,
2284}
2285
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002286
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002287class ChangeDescription(object):
2288 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002289 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002290 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002291
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002292 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002293 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002294
agable@chromium.org42c20792013-09-12 17:34:49 +00002295 @property # www.logilab.org/ticket/89786
2296 def description(self): # pylint: disable=E0202
2297 return '\n'.join(self._description_lines)
2298
2299 def set_description(self, desc):
2300 if isinstance(desc, basestring):
2301 lines = desc.splitlines()
2302 else:
2303 lines = [line.rstrip() for line in desc]
2304 while lines and not lines[0]:
2305 lines.pop(0)
2306 while lines and not lines[-1]:
2307 lines.pop(-1)
2308 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002309
piman@chromium.org336f9122014-09-04 02:16:55 +00002310 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002311 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002312 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002313 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002314 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002315 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002316
agable@chromium.org42c20792013-09-12 17:34:49 +00002317 # Get the set of R= and TBR= lines and remove them from the desciption.
2318 regexp = re.compile(self.R_LINE)
2319 matches = [regexp.match(line) for line in self._description_lines]
2320 new_desc = [l for i, l in enumerate(self._description_lines)
2321 if not matches[i]]
2322 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002323
agable@chromium.org42c20792013-09-12 17:34:49 +00002324 # Construct new unified R= and TBR= lines.
2325 r_names = []
2326 tbr_names = []
2327 for match in matches:
2328 if not match:
2329 continue
2330 people = cleanup_list([match.group(2).strip()])
2331 if match.group(1) == 'TBR':
2332 tbr_names.extend(people)
2333 else:
2334 r_names.extend(people)
2335 for name in r_names:
2336 if name not in reviewers:
2337 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002338 if add_owners_tbr:
2339 owners_db = owners.Database(change.RepositoryRoot(),
2340 fopen=file, os_path=os.path, glob=glob.glob)
2341 all_reviewers = set(tbr_names + reviewers)
2342 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2343 all_reviewers)
2344 tbr_names.extend(owners_db.reviewers_for(missing_files,
2345 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002346 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2347 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2348
2349 # Put the new lines in the description where the old first R= line was.
2350 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2351 if 0 <= line_loc < len(self._description_lines):
2352 if new_tbr_line:
2353 self._description_lines.insert(line_loc, new_tbr_line)
2354 if new_r_line:
2355 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002356 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002357 if new_r_line:
2358 self.append_footer(new_r_line)
2359 if new_tbr_line:
2360 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002361
2362 def prompt(self):
2363 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002364 self.set_description([
2365 '# Enter a description of the change.',
2366 '# This will be displayed on the codereview site.',
2367 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002368 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002369 '--------------------',
2370 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002371
agable@chromium.org42c20792013-09-12 17:34:49 +00002372 regexp = re.compile(self.BUG_LINE)
2373 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00002374 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00002375 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002376 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002377 if not content:
2378 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002379 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002380
2381 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002382 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2383 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002384 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002385 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002386
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002387 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00002388 if self._description_lines:
2389 # Add an empty line if either the last line or the new line isn't a tag.
2390 last_line = self._description_lines[-1]
2391 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
2392 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2393 self._description_lines.append('')
2394 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002395
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002396 def get_reviewers(self):
2397 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002398 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
2399 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002400 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002401
2402
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002403def get_approving_reviewers(props):
2404 """Retrieves the reviewers that approved a CL from the issue properties with
2405 messages.
2406
2407 Note that the list may contain reviewers that are not committer, thus are not
2408 considered by the CQ.
2409 """
2410 return sorted(
2411 set(
2412 message['sender']
2413 for message in props['messages']
2414 if message['approval'] and message['sender'] in props['reviewers']
2415 )
2416 )
2417
2418
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002419def FindCodereviewSettingsFile(filename='codereview.settings'):
2420 """Finds the given file starting in the cwd and going up.
2421
2422 Only looks up to the top of the repository unless an
2423 'inherit-review-settings-ok' file exists in the root of the repository.
2424 """
2425 inherit_ok_file = 'inherit-review-settings-ok'
2426 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002427 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002428 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2429 root = '/'
2430 while True:
2431 if filename in os.listdir(cwd):
2432 if os.path.isfile(os.path.join(cwd, filename)):
2433 return open(os.path.join(cwd, filename))
2434 if cwd == root:
2435 break
2436 cwd = os.path.dirname(cwd)
2437
2438
2439def LoadCodereviewSettingsFromFile(fileobj):
2440 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002441 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002442
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002443 def SetProperty(name, setting, unset_error_ok=False):
2444 fullname = 'rietveld.' + name
2445 if setting in keyvals:
2446 RunGit(['config', fullname, keyvals[setting]])
2447 else:
2448 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2449
2450 SetProperty('server', 'CODE_REVIEW_SERVER')
2451 # Only server setting is required. Other settings can be absent.
2452 # In that case, we ignore errors raised during option deletion attempt.
2453 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002454 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002455 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2456 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002457 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002458 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002459 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2460 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002461 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002462 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002463 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002464 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2465 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002466
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002467 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002468 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002469
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002470 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
2471 RunGit(['config', 'gerrit.squash-uploads',
2472 keyvals['GERRIT_SQUASH_UPLOADS']])
2473
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002474 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2475 #should be of the form
2476 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2477 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2478 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2479 keyvals['ORIGIN_URL_CONFIG']])
2480
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002481
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002482def urlretrieve(source, destination):
2483 """urllib is broken for SSL connections via a proxy therefore we
2484 can't use urllib.urlretrieve()."""
2485 with open(destination, 'w') as f:
2486 f.write(urllib2.urlopen(source).read())
2487
2488
ukai@chromium.org712d6102013-11-27 00:52:58 +00002489def hasSheBang(fname):
2490 """Checks fname is a #! script."""
2491 with open(fname) as f:
2492 return f.read(2).startswith('#!')
2493
2494
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00002495# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
2496def DownloadHooks(*args, **kwargs):
2497 pass
2498
2499
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002500def DownloadGerritHook(force):
2501 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002502
2503 Args:
2504 force: True to update hooks. False to install hooks if not present.
2505 """
2506 if not settings.GetIsGerrit():
2507 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00002508 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002509 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2510 if not os.access(dst, os.X_OK):
2511 if os.path.exists(dst):
2512 if not force:
2513 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002514 try:
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002515 print(
2516 'WARNING: installing Gerrit commit-msg hook.\n'
2517 ' This behavior of git cl will soon be disabled.\n'
2518 ' See bug http://crbug.com/579176.')
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002519 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002520 if not hasSheBang(dst):
2521 DieWithError('Not a script: %s\n'
2522 'You need to download from\n%s\n'
2523 'into .git/hooks/commit-msg and '
2524 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002525 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2526 except Exception:
2527 if os.path.exists(dst):
2528 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002529 DieWithError('\nFailed to download hooks.\n'
2530 'You need to download from\n%s\n'
2531 'into .git/hooks/commit-msg and '
2532 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002533
2534
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002535
2536def GetRietveldCodereviewSettingsInteractively():
2537 """Prompt the user for settings."""
2538 server = settings.GetDefaultServerUrl(error_ok=True)
2539 prompt = 'Rietveld server (host[:port])'
2540 prompt += ' [%s]' % (server or DEFAULT_SERVER)
2541 newserver = ask_for_data(prompt + ':')
2542 if not server and not newserver:
2543 newserver = DEFAULT_SERVER
2544 if newserver:
2545 newserver = gclient_utils.UpgradeToHttps(newserver)
2546 if newserver != server:
2547 RunGit(['config', 'rietveld.server', newserver])
2548
2549 def SetProperty(initial, caption, name, is_url):
2550 prompt = caption
2551 if initial:
2552 prompt += ' ("x" to clear) [%s]' % initial
2553 new_val = ask_for_data(prompt + ':')
2554 if new_val == 'x':
2555 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
2556 elif new_val:
2557 if is_url:
2558 new_val = gclient_utils.UpgradeToHttps(new_val)
2559 if new_val != initial:
2560 RunGit(['config', 'rietveld.' + name, new_val])
2561
2562 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
2563 SetProperty(settings.GetDefaultPrivateFlag(),
2564 'Private flag (rietveld only)', 'private', False)
2565 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
2566 'tree-status-url', False)
2567 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
2568 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
2569 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
2570 'run-post-upload-hook', False)
2571
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002572@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002573def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002574 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002575
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002576 print('WARNING: git cl config works for Rietveld only.\n'
2577 'For Gerrit, see http://crbug.com/579160.')
2578 # TODO(tandrii): add Gerrit support as part of http://crbug.com/579160.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00002579 parser.add_option('--activate-update', action='store_true',
2580 help='activate auto-updating [rietveld] section in '
2581 '.git/config')
2582 parser.add_option('--deactivate-update', action='store_true',
2583 help='deactivate auto-updating [rietveld] section in '
2584 '.git/config')
2585 options, args = parser.parse_args(args)
2586
2587 if options.deactivate_update:
2588 RunGit(['config', 'rietveld.autoupdate', 'false'])
2589 return
2590
2591 if options.activate_update:
2592 RunGit(['config', '--unset', 'rietveld.autoupdate'])
2593 return
2594
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002595 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002596 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002597 return 0
2598
2599 url = args[0]
2600 if not url.endswith('codereview.settings'):
2601 url = os.path.join(url, 'codereview.settings')
2602
2603 # Load code review settings and download hooks (if available).
2604 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
2605 return 0
2606
2607
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002608def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002609 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002610 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
2611 branch = ShortBranchName(branchref)
2612 _, args = parser.parse_args(args)
2613 if not args:
2614 print("Current base-url:")
2615 return RunGit(['config', 'branch.%s.base-url' % branch],
2616 error_ok=False).strip()
2617 else:
2618 print("Setting base-url to %s" % args[0])
2619 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
2620 error_ok=False).strip()
2621
2622
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002623def color_for_status(status):
2624 """Maps a Changelist status to color, for CMDstatus and other tools."""
2625 return {
2626 'unsent': Fore.RED,
2627 'waiting': Fore.BLUE,
2628 'reply': Fore.YELLOW,
2629 'lgtm': Fore.GREEN,
2630 'commit': Fore.MAGENTA,
2631 'closed': Fore.CYAN,
2632 'error': Fore.WHITE,
2633 }.get(status, Fore.WHITE)
2634
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002635def fetch_cl_status(branch, auth_config=None):
2636 """Fetches information for an issue and returns (branch, issue, status)."""
2637 cl = Changelist(branchref=branch, auth_config=auth_config)
2638 url = cl.GetIssueURL()
2639 status = cl.GetStatus()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002640
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002641 if url and (not status or status == 'error'):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002642 # The issue probably doesn't exist anymore.
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002643 url += ' (broken)'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002644
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002645 return (branch, url, status)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002646
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002647def get_cl_statuses(
2648 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002649 """Returns a blocking iterable of (branch, issue, color) for given branches.
2650
2651 If fine_grained is true, this will fetch CL statuses from the server.
2652 Otherwise, simply indicate if there's a matching url for the given branches.
2653
2654 If max_processes is specified, it is used as the maximum number of processes
2655 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
2656 spawned.
2657 """
2658 # Silence upload.py otherwise it becomes unwieldly.
2659 upload.verbosity = 0
2660
2661 if fine_grained:
2662 # Process one branch synchronously to work through authentication, then
2663 # spawn processes to process all the other branches in parallel.
2664 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002665 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
2666 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002667
2668 branches_to_fetch = branches[1:]
2669 pool = ThreadPool(
2670 min(max_processes, len(branches_to_fetch))
2671 if max_processes is not None
2672 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002673 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002674 yield x
2675 else:
2676 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
2677 for b in branches:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002678 cl = Changelist(branchref=b, auth_config=auth_config)
2679 url = cl.GetIssueURL()
2680 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002681
rmistry@google.com2dd99862015-06-22 12:22:18 +00002682
2683def upload_branch_deps(cl, args):
2684 """Uploads CLs of local branches that are dependents of the current branch.
2685
2686 If the local branch dependency tree looks like:
2687 test1 -> test2.1 -> test3.1
2688 -> test3.2
2689 -> test2.2 -> test3.3
2690
2691 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
2692 run on the dependent branches in this order:
2693 test2.1, test3.1, test3.2, test2.2, test3.3
2694
2695 Note: This function does not rebase your local dependent branches. Use it when
2696 you make a change to the parent branch that will not conflict with its
2697 dependent branches, and you would like their dependencies updated in
2698 Rietveld.
2699 """
2700 if git_common.is_dirty_git_tree('upload-branch-deps'):
2701 return 1
2702
2703 root_branch = cl.GetBranch()
2704 if root_branch is None:
2705 DieWithError('Can\'t find dependent branches from detached HEAD state. '
2706 'Get on a branch!')
2707 if not cl.GetIssue() or not cl.GetPatchset():
2708 DieWithError('Current branch does not have an uploaded CL. We cannot set '
2709 'patchset dependencies without an uploaded CL.')
2710
2711 branches = RunGit(['for-each-ref',
2712 '--format=%(refname:short) %(upstream:short)',
2713 'refs/heads'])
2714 if not branches:
2715 print('No local branches found.')
2716 return 0
2717
2718 # Create a dictionary of all local branches to the branches that are dependent
2719 # on it.
2720 tracked_to_dependents = collections.defaultdict(list)
2721 for b in branches.splitlines():
2722 tokens = b.split()
2723 if len(tokens) == 2:
2724 branch_name, tracked = tokens
2725 tracked_to_dependents[tracked].append(branch_name)
2726
2727 print
2728 print 'The dependent local branches of %s are:' % root_branch
2729 dependents = []
2730 def traverse_dependents_preorder(branch, padding=''):
2731 dependents_to_process = tracked_to_dependents.get(branch, [])
2732 padding += ' '
2733 for dependent in dependents_to_process:
2734 print '%s%s' % (padding, dependent)
2735 dependents.append(dependent)
2736 traverse_dependents_preorder(dependent, padding)
2737 traverse_dependents_preorder(root_branch)
2738 print
2739
2740 if not dependents:
2741 print 'There are no dependent local branches for %s' % root_branch
2742 return 0
2743
2744 print ('This command will checkout all dependent branches and run '
2745 '"git cl upload".')
2746 ask_for_data('[Press enter to continue or ctrl-C to quit]')
2747
andybons@chromium.org962f9462016-02-03 20:00:42 +00002748 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00002749 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00002750 args.extend(['-t', 'Updated patchset dependency'])
2751
rmistry@google.com2dd99862015-06-22 12:22:18 +00002752 # Record all dependents that failed to upload.
2753 failures = {}
2754 # Go through all dependents, checkout the branch and upload.
2755 try:
2756 for dependent_branch in dependents:
2757 print
2758 print '--------------------------------------'
2759 print 'Running "git cl upload" from %s:' % dependent_branch
2760 RunGit(['checkout', '-q', dependent_branch])
2761 print
2762 try:
2763 if CMDupload(OptionParser(), args) != 0:
2764 print 'Upload failed for %s!' % dependent_branch
2765 failures[dependent_branch] = 1
2766 except: # pylint: disable=W0702
2767 failures[dependent_branch] = 1
2768 print
2769 finally:
2770 # Swap back to the original root branch.
2771 RunGit(['checkout', '-q', root_branch])
2772
2773 print
2774 print 'Upload complete for dependent branches!'
2775 for dependent_branch in dependents:
2776 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
2777 print ' %s : %s' % (dependent_branch, upload_status)
2778 print
2779
2780 return 0
2781
2782
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002783def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002784 """Show status of changelists.
2785
2786 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00002787 - Red not sent for review or broken
2788 - Blue waiting for review
2789 - Yellow waiting for you to reply to review
2790 - Green LGTM'ed
2791 - Magenta in the commit queue
2792 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002793
2794 Also see 'git cl comments'.
2795 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002796 parser.add_option('--field',
2797 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002798 parser.add_option('-f', '--fast', action='store_true',
2799 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002800 parser.add_option(
2801 '-j', '--maxjobs', action='store', type=int,
2802 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002803
2804 auth.add_auth_options(parser)
2805 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002806 if args:
2807 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002808 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002809
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002810 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002811 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002812 if options.field.startswith('desc'):
2813 print cl.GetDescription()
2814 elif options.field == 'id':
2815 issueid = cl.GetIssue()
2816 if issueid:
2817 print issueid
2818 elif options.field == 'patch':
2819 patchset = cl.GetPatchset()
2820 if patchset:
2821 print patchset
2822 elif options.field == 'url':
2823 url = cl.GetIssueURL()
2824 if url:
2825 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002826 return 0
2827
2828 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
2829 if not branches:
2830 print('No local branch found.')
2831 return 0
2832
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002833 changes = (
2834 Changelist(branchref=b, auth_config=auth_config)
2835 for b in branches.splitlines())
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002836 # TODO(tandrii): refactor to use CLs list instead of branches list.
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00002837 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002838 alignment = max(5, max(len(b) for b in branches))
2839 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002840 output = get_cl_statuses(branches,
2841 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002842 max_processes=options.maxjobs,
2843 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002844
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002845 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002846 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002847 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002848 while branch not in branch_statuses:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002849 b, i, status = output.next()
2850 branch_statuses[b] = (i, status)
2851 issue_url, status = branch_statuses.pop(branch)
2852 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00002853 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00002854 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00002855 color = ''
2856 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002857 status_str = '(%s)' % status if status else ''
2858 print ' %*s : %s%s %s%s' % (
2859 alignment, ShortBranchName(branch), color, issue_url, status_str,
2860 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002861
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002862 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002863 print
2864 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002865 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00002866 if not cl.GetIssue():
2867 print 'No issue assigned.'
2868 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002869 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00002870 if not options.fast:
2871 print 'Issue description:'
2872 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002873 return 0
2874
2875
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002876def colorize_CMDstatus_doc():
2877 """To be called once in main() to add colors to git cl status help."""
2878 colors = [i for i in dir(Fore) if i[0].isupper()]
2879
2880 def colorize_line(line):
2881 for color in colors:
2882 if color in line.upper():
2883 # Extract whitespaces first and the leading '-'.
2884 indent = len(line) - len(line.lstrip(' ')) + 1
2885 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
2886 return line
2887
2888 lines = CMDstatus.__doc__.splitlines()
2889 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
2890
2891
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002892@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002893def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002894 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002895
2896 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002897 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00002898 parser.add_option('-r', '--reverse', action='store_true',
2899 help='Lookup the branch(es) for the specified issues. If '
2900 'no issues are specified, all branches with mapped '
2901 'issues will be listed.')
2902 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002903
dnj@chromium.org406c4402015-03-03 17:22:28 +00002904 if options.reverse:
2905 branches = RunGit(['for-each-ref', 'refs/heads',
2906 '--format=%(refname:short)']).splitlines()
2907
2908 # Reverse issue lookup.
2909 issue_branch_map = {}
2910 for branch in branches:
2911 cl = Changelist(branchref=branch)
2912 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
2913 if not args:
2914 args = sorted(issue_branch_map.iterkeys())
2915 for issue in args:
2916 if not issue:
2917 continue
2918 print 'Branch for issue number %s: %s' % (
2919 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
2920 else:
2921 cl = Changelist()
2922 if len(args) > 0:
2923 try:
2924 issue = int(args[0])
2925 except ValueError:
2926 DieWithError('Pass a number to set the issue or none to list it.\n'
2927 'Maybe you want to run git cl status?')
2928 cl.SetIssue(issue)
2929 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002930 return 0
2931
2932
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002933def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002934 """Shows or posts review comments for any changelist."""
2935 parser.add_option('-a', '--add-comment', dest='comment',
2936 help='comment to add to an issue')
2937 parser.add_option('-i', dest='issue',
2938 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00002939 parser.add_option('-j', '--json-file',
2940 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002941 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002942 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002943 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002944
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002945 issue = None
2946 if options.issue:
2947 try:
2948 issue = int(options.issue)
2949 except ValueError:
2950 DieWithError('A review issue id is expected to be a number')
2951
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002952 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002953
2954 if options.comment:
2955 cl.AddComment(options.comment)
2956 return 0
2957
2958 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00002959 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00002960 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00002961 summary.append({
2962 'date': message['date'],
2963 'lgtm': False,
2964 'message': message['text'],
2965 'not_lgtm': False,
2966 'sender': message['sender'],
2967 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002968 if message['disapproval']:
2969 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00002970 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002971 elif message['approval']:
2972 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00002973 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002974 elif message['sender'] == data['owner_email']:
2975 color = Fore.MAGENTA
2976 else:
2977 color = Fore.BLUE
2978 print '\n%s%s %s%s' % (
2979 color, message['date'].split('.', 1)[0], message['sender'],
2980 Fore.RESET)
2981 if message['text'].strip():
2982 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00002983 if options.json_file:
2984 with open(options.json_file, 'wb') as f:
2985 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002986 return 0
2987
2988
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002989def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002990 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00002991 parser.add_option('-d', '--display', action='store_true',
2992 help='Display the description instead of opening an editor')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002993 auth.add_auth_options(parser)
2994 options, _ = parser.parse_args(args)
2995 auth_config = auth.extract_auth_config_from_options(options)
2996 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002997 if not cl.GetIssue():
2998 DieWithError('This branch has no associated changelist.')
2999 description = ChangeDescription(cl.GetDescription())
smut@google.com34fb6b12015-07-13 20:03:26 +00003000 if options.display:
3001 print description.description
3002 return 0
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003003 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003004 if cl.GetDescription() != description.description:
3005 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003006 return 0
3007
3008
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003009def CreateDescriptionFromLog(args):
3010 """Pulls out the commit log to use as a base for the CL description."""
3011 log_args = []
3012 if len(args) == 1 and not args[0].endswith('.'):
3013 log_args = [args[0] + '..']
3014 elif len(args) == 1 and args[0].endswith('...'):
3015 log_args = [args[0][:-1]]
3016 elif len(args) == 2:
3017 log_args = [args[0] + '..' + args[1]]
3018 else:
3019 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003020 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003021
3022
thestig@chromium.org44202a22014-03-11 19:22:18 +00003023def CMDlint(parser, args):
3024 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003025 parser.add_option('--filter', action='append', metavar='-x,+y',
3026 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003027 auth.add_auth_options(parser)
3028 options, args = parser.parse_args(args)
3029 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003030
3031 # Access to a protected member _XX of a client class
3032 # pylint: disable=W0212
3033 try:
3034 import cpplint
3035 import cpplint_chromium
3036 except ImportError:
3037 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
3038 return 1
3039
3040 # Change the current working directory before calling lint so that it
3041 # shows the correct base.
3042 previous_cwd = os.getcwd()
3043 os.chdir(settings.GetRoot())
3044 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003045 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003046 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3047 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003048 if not files:
3049 print "Cannot lint an empty CL"
3050 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003051
3052 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003053 command = args + files
3054 if options.filter:
3055 command = ['--filter=' + ','.join(options.filter)] + command
3056 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003057
3058 white_regex = re.compile(settings.GetLintRegex())
3059 black_regex = re.compile(settings.GetLintIgnoreRegex())
3060 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3061 for filename in filenames:
3062 if white_regex.match(filename):
3063 if black_regex.match(filename):
3064 print "Ignoring file %s" % filename
3065 else:
3066 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3067 extra_check_functions)
3068 else:
3069 print "Skipping file %s" % filename
3070 finally:
3071 os.chdir(previous_cwd)
3072 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
3073 if cpplint._cpplint_state.error_count != 0:
3074 return 1
3075 return 0
3076
3077
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003078def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003079 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003080 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003081 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003082 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003083 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003084 auth.add_auth_options(parser)
3085 options, args = parser.parse_args(args)
3086 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003087
sbc@chromium.org71437c02015-04-09 19:29:40 +00003088 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00003089 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003090 return 1
3091
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003092 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003093 if args:
3094 base_branch = args[0]
3095 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003096 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003097 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003098
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003099 cl.RunHook(
3100 committing=not options.upload,
3101 may_prompt=False,
3102 verbose=options.verbose,
3103 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003104 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003105
3106
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00003107def AddChangeIdToCommitMessage(options, args):
3108 """Re-commits using the current message, assumes the commit hook is in
3109 place.
3110 """
3111 log_desc = options.message or CreateDescriptionFromLog(args)
3112 git_command = ['commit', '--amend', '-m', log_desc]
3113 RunGit(git_command)
3114 new_log_desc = CreateDescriptionFromLog(args)
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003115 if git_footers.get_footer_change_id(new_log_desc):
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00003116 print 'git-cl: Added Change-Id to commit message.'
tandrii@chromium.orga342c922016-03-16 07:08:25 +00003117 return new_log_desc
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00003118 else:
3119 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
3120
3121
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003122def GenerateGerritChangeId(message):
3123 """Returns Ixxxxxx...xxx change id.
3124
3125 Works the same way as
3126 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3127 but can be called on demand on all platforms.
3128
3129 The basic idea is to generate git hash of a state of the tree, original commit
3130 message, author/committer info and timestamps.
3131 """
3132 lines = []
3133 tree_hash = RunGitSilent(['write-tree'])
3134 lines.append('tree %s' % tree_hash.strip())
3135 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3136 if code == 0:
3137 lines.append('parent %s' % parent.strip())
3138 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3139 lines.append('author %s' % author.strip())
3140 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3141 lines.append('committer %s' % committer.strip())
3142 lines.append('')
3143 # Note: Gerrit's commit-hook actually cleans message of some lines and
3144 # whitespace. This code is not doing this, but it clearly won't decrease
3145 # entropy.
3146 lines.append(message)
3147 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3148 stdin='\n'.join(lines))
3149 return 'I%s' % change_hash.strip()
3150
3151
wittman@chromium.org455dc922015-01-26 20:15:50 +00003152def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3153 """Computes the remote branch ref to use for the CL.
3154
3155 Args:
3156 remote (str): The git remote for the CL.
3157 remote_branch (str): The git remote branch for the CL.
3158 target_branch (str): The target branch specified by the user.
3159 pending_prefix (str): The pending prefix from the settings.
3160 """
3161 if not (remote and remote_branch):
3162 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003163
wittman@chromium.org455dc922015-01-26 20:15:50 +00003164 if target_branch:
3165 # Cannonicalize branch references to the equivalent local full symbolic
3166 # refs, which are then translated into the remote full symbolic refs
3167 # below.
3168 if '/' not in target_branch:
3169 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3170 else:
3171 prefix_replacements = (
3172 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3173 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3174 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3175 )
3176 match = None
3177 for regex, replacement in prefix_replacements:
3178 match = re.search(regex, target_branch)
3179 if match:
3180 remote_branch = target_branch.replace(match.group(0), replacement)
3181 break
3182 if not match:
3183 # This is a branch path but not one we recognize; use as-is.
3184 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003185 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3186 # Handle the refs that need to land in different refs.
3187 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003188
wittman@chromium.org455dc922015-01-26 20:15:50 +00003189 # Create the true path to the remote branch.
3190 # Does the following translation:
3191 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3192 # * refs/remotes/origin/master -> refs/heads/master
3193 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3194 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3195 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3196 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3197 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3198 'refs/heads/')
3199 elif remote_branch.startswith('refs/remotes/branch-heads'):
3200 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3201 # If a pending prefix exists then replace refs/ with it.
3202 if pending_prefix:
3203 remote_branch = remote_branch.replace('refs/', pending_prefix)
3204 return remote_branch
3205
3206
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003207def cleanup_list(l):
3208 """Fixes a list so that comma separated items are put as individual items.
3209
3210 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3211 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3212 """
3213 items = sum((i.split(',') for i in l), [])
3214 stripped_items = (i.strip() for i in items)
3215 return sorted(filter(None, stripped_items))
3216
3217
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003218@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003219def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003220 """Uploads the current changelist to codereview.
3221
3222 Can skip dependency patchset uploads for a branch by running:
3223 git config branch.branch_name.skip-deps-uploads True
3224 To unset run:
3225 git config --unset branch.branch_name.skip-deps-uploads
3226 Can also set the above globally by using the --global flag.
3227 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003228 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3229 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003230 parser.add_option('--bypass-watchlists', action='store_true',
3231 dest='bypass_watchlists',
3232 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003233 parser.add_option('-f', action='store_true', dest='force',
3234 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003235 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003236 parser.add_option('-t', dest='title',
3237 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003238 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003239 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003240 help='reviewer email addresses')
3241 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003242 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003243 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003244 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003245 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003246 parser.add_option('--emulate_svn_auto_props',
3247 '--emulate-svn-auto-props',
3248 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003249 dest="emulate_svn_auto_props",
3250 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003251 parser.add_option('-c', '--use-commit-queue', action='store_true',
3252 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003253 parser.add_option('--private', action='store_true',
3254 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003255 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003256 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003257 metavar='TARGET',
3258 help='Apply CL to remote ref TARGET. ' +
3259 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003260 parser.add_option('--squash', action='store_true',
3261 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003262 parser.add_option('--no-squash', action='store_true',
3263 help='Don\'t squash multiple commits into one ' +
3264 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003265 parser.add_option('--email', default=None,
3266 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003267 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3268 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003269 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3270 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003271 help='Send the patchset to do a CQ dry run right after '
3272 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003273 parser.add_option('--dependencies', action='store_true',
3274 help='Uploads CLs of all the local branches that depend on '
3275 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003276
rmistry@google.com2dd99862015-06-22 12:22:18 +00003277 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003278 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003279 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003280 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003281 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003282
sbc@chromium.org71437c02015-04-09 19:29:40 +00003283 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003284 return 1
3285
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003286 options.reviewers = cleanup_list(options.reviewers)
3287 options.cc = cleanup_list(options.cc)
3288
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003289 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3290 settings.GetIsGerrit()
3291
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003292 cl = Changelist(auth_config=auth_config)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003293 if args:
3294 # TODO(ukai): is it ok for gerrit case?
3295 base_branch = args[0]
3296 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00003297 if cl.GetBranch() is None:
3298 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
3299
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003300 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003301 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00003302 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00003303
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003304 # Make sure authenticated to Rietveld before running expensive hooks. It is
3305 # a fast, best efforts check. Rietveld still can reject the authentication
3306 # during the actual upload.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003307 if not cl.IsGerrit() and auth_config.use_oauth2:
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003308 authenticator = auth.get_authenticator_for_host(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003309 cl.GetCodereviewServer(), auth_config)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003310 if not authenticator.has_cached_credentials():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003311 raise auth.LoginRequiredError(cl.GetCodereviewServer())
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003312
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003313 # Apply watchlists on upload.
3314 change = cl.GetChange(base_branch, None)
3315 watchlist = watchlists.Watchlists(change.RepositoryRoot())
3316 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003317 if not options.bypass_watchlists:
3318 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003319
ukai@chromium.orge8077812012-02-03 03:41:46 +00003320 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00003321 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00003322 # Set the reviewer list now so that presubmit checks can access it.
3323 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00003324 change_description.update_reviewers(options.reviewers,
3325 options.tbr_owners,
3326 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00003327 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003328 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00003329 may_prompt=not options.force,
3330 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003331 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003332 if not hook_results.should_continue():
3333 return 1
3334 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003335 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003336
koz@chromium.org5974d7a2013-04-02 20:50:37 +00003337 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003338 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00003339 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00003340 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00003341 print ('The last upload made from this repository was patchset #%d but '
3342 'the most recent patchset on the server is #%d.'
3343 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00003344 print ('Uploading will still work, but if you\'ve uploaded to this issue '
3345 'from another machine or branch the patch you\'re uploading now '
3346 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00003347 ask_for_data('About to upload; enter to confirm.')
3348
iannucci@chromium.org79540052012-10-19 23:15:26 +00003349 print_stats(options.similarity, options.find_copies, args)
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003350 if cl.IsGerrit():
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003351 if options.squash and options.no_squash:
3352 DieWithError('Can only use one of --squash or --no-squash')
3353
3354 options.squash = ((settings.GetSquashGerritUploads() or options.squash) and
3355 not options.no_squash)
3356
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003357 ret = cl.CMDUploadChange(options, args, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00003358 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00003359 git_set_branch_value('last-upload-hash',
3360 RunGit(['rev-parse', 'HEAD']).strip())
rmistry@google.com5626a922015-02-26 14:03:30 +00003361 # Run post upload hooks, if specified.
3362 if settings.GetRunPostUploadHook():
3363 presubmit_support.DoPostUploadExecuter(
3364 change,
3365 cl,
3366 settings.GetRoot(),
3367 options.verbose,
3368 sys.stdout)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00003369
rmistry@google.com2dd99862015-06-22 12:22:18 +00003370 # Upload all dependencies if specified.
3371 if options.dependencies:
3372 print
3373 print '--dependencies has been specified.'
3374 print 'All dependent local branches will be re-uploaded.'
3375 print
3376 # Remove the dependencies flag from args so that we do not end up in a
3377 # loop.
3378 orig_args.remove('--dependencies')
3379 upload_branch_deps(cl, orig_args)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00003380 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00003381
3382
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003383def IsSubmoduleMergeCommit(ref):
3384 # When submodules are added to the repo, we expect there to be a single
3385 # non-git-svn merge commit at remote HEAD with a signature comment.
3386 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003387 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003388 return RunGit(cmd) != ''
3389
3390
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003391def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003392 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003393
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003394 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3395 upstream and closes the issue automatically and atomically.
3396
3397 Otherwise (in case of Rietveld):
3398 Squashes branch into a single commit.
3399 Updates changelog with metadata (e.g. pointer to review).
3400 Pushes/dcommits the code upstream.
3401 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003402 """
3403 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3404 help='bypass upload presubmit hook')
3405 parser.add_option('-m', dest='message',
3406 help="override review description")
3407 parser.add_option('-f', action='store_true', dest='force',
3408 help="force yes to questions (don't prompt)")
3409 parser.add_option('-c', dest='contributor',
3410 help="external contributor for patch (appended to " +
3411 "description and used as author for git). Should be " +
3412 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003413 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003414 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003415 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003416 auth_config = auth.extract_auth_config_from_options(options)
3417
3418 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003419
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003420 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3421 if cl.IsGerrit():
3422 if options.message:
3423 # This could be implemented, but it requires sending a new patch to
3424 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3425 # Besides, Gerrit has the ability to change the commit message on submit
3426 # automatically, thus there is no need to support this option (so far?).
3427 parser.error('-m MESSAGE option is not supported for Gerrit.')
3428 if options.contributor:
3429 parser.error(
3430 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3431 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3432 'the contributor\'s "name <email>". If you can\'t upload such a '
3433 'commit for review, contact your repository admin and request'
3434 '"Forge-Author" permission.')
3435 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3436 options.verbose)
3437
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003438 current = cl.GetBranch()
3439 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3440 if not settings.GetIsGitSvn() and remote == '.':
3441 print
3442 print 'Attempting to push branch %r into another local branch!' % current
3443 print
3444 print 'Either reparent this branch on top of origin/master:'
3445 print ' git reparent-branch --root'
3446 print
3447 print 'OR run `git rebase-update` if you think the parent branch is already'
3448 print 'committed.'
3449 print
3450 print ' Current parent: %r' % upstream_branch
3451 return 1
3452
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003453 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003454 # Default to merging against our best guess of the upstream branch.
3455 args = [cl.GetUpstreamBranch()]
3456
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003457 if options.contributor:
3458 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
3459 print "Please provide contibutor as 'First Last <email@example.com>'"
3460 return 1
3461
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003462 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003463 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003464
sbc@chromium.org71437c02015-04-09 19:29:40 +00003465 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003466 return 1
3467
3468 # This rev-list syntax means "show all commits not in my branch that
3469 # are in base_branch".
3470 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3471 base_branch]).splitlines()
3472 if upstream_commits:
3473 print ('Base branch "%s" has %d commits '
3474 'not in this branch.' % (base_branch, len(upstream_commits)))
3475 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
3476 return 1
3477
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003478 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003479 svn_head = None
3480 if cmd == 'dcommit' or base_has_submodules:
3481 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
3482 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003483
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003484 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003485 # If the base_head is a submodule merge commit, the first parent of the
3486 # base_head should be a git-svn commit, which is what we're interested in.
3487 base_svn_head = base_branch
3488 if base_has_submodules:
3489 base_svn_head += '^1'
3490
3491 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003492 if extra_commits:
3493 print ('This branch has %d additional commits not upstreamed yet.'
3494 % len(extra_commits.splitlines()))
3495 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
3496 'before attempting to %s.' % (base_branch, cmd))
3497 return 1
3498
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003499 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003500 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003501 author = None
3502 if options.contributor:
3503 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003504 hook_results = cl.RunHook(
3505 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003506 may_prompt=not options.force,
3507 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003508 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003509 if not hook_results.should_continue():
3510 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003511
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003512 # Check the tree status if the tree status URL is set.
3513 status = GetTreeStatus()
3514 if 'closed' == status:
3515 print('The tree is closed. Please wait for it to reopen. Use '
3516 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3517 return 1
3518 elif 'unknown' == status:
3519 print('Unable to determine tree status. Please verify manually and '
3520 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3521 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003522
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003523 change_desc = ChangeDescription(options.message)
3524 if not change_desc.description and cl.GetIssue():
3525 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003526
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003527 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00003528 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003529 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00003530 else:
3531 print 'No description set.'
3532 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
3533 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003534
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003535 # Keep a separate copy for the commit message, because the commit message
3536 # contains the link to the Rietveld issue, while the Rietveld message contains
3537 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003538 # Keep a separate copy for the commit message.
3539 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00003540 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003541
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003542 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00003543 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00003544 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00003545 # after it. Add a period on a new line to circumvent this. Also add a space
3546 # before the period to make sure that Gitiles continues to correctly resolve
3547 # the URL.
3548 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003549 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003550 commit_desc.append_footer('Patch from %s.' % options.contributor)
3551
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00003552 print('Description:')
3553 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003554
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003555 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003556 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00003557 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003558
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003559 # We want to squash all this branch's commits into one commit with the proper
3560 # description. We do this by doing a "reset --soft" to the base branch (which
3561 # keeps the working copy the same), then dcommitting that. If origin/master
3562 # has a submodule merge commit, we'll also need to cherry-pick the squashed
3563 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003564 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003565 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
3566 # Delete the branches if they exist.
3567 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
3568 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
3569 result = RunGitWithCode(showref_cmd)
3570 if result[0] == 0:
3571 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003572
3573 # We might be in a directory that's present in this branch but not in the
3574 # trunk. Move up to the top of the tree so that git commands that expect a
3575 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003576 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003577 if rel_base_path:
3578 os.chdir(rel_base_path)
3579
3580 # Stuff our change into the merge branch.
3581 # We wrap in a try...finally block so if anything goes wrong,
3582 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003583 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003584 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003585 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003586 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003587 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00003588 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003589 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003590 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003591 RunGit(
3592 [
3593 'commit', '--author', options.contributor,
3594 '-m', commit_desc.description,
3595 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003596 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003597 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003598 if base_has_submodules:
3599 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
3600 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
3601 RunGit(['checkout', CHERRY_PICK_BRANCH])
3602 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003603 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003604 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003605 mirror = settings.GetGitMirror(remote)
3606 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003607 pending_prefix = settings.GetPendingRefPrefix()
3608 if not pending_prefix or branch.startswith(pending_prefix):
3609 # If not using refs/pending/heads/* at all, or target ref is already set
3610 # to pending, then push to the target ref directly.
3611 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003612 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003613 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003614 else:
3615 # Cherry-pick the change on top of pending ref and then push it.
3616 assert branch.startswith('refs/'), branch
3617 assert pending_prefix[-1] == '/', pending_prefix
3618 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003619 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003620 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003621 if retcode == 0:
3622 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003623 else:
3624 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003625 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003626 'svn', 'dcommit',
3627 '-C%s' % options.similarity,
3628 '--no-rebase', '--rmdir',
3629 ]
3630 if settings.GetForceHttpsCommitUrl():
3631 # Allow forcing https commit URLs for some projects that don't allow
3632 # committing to http URLs (like Google Code).
3633 remote_url = cl.GetGitSvnRemoteUrl()
3634 if urlparse.urlparse(remote_url).scheme == 'http':
3635 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003636 cmd_args.append('--commit-url=%s' % remote_url)
3637 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003638 if 'Committed r' in output:
3639 revision = re.match(
3640 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
3641 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003642 finally:
3643 # And then swap back to the original branch and clean up.
3644 RunGit(['checkout', '-q', cl.GetBranch()])
3645 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003646 if base_has_submodules:
3647 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003648
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003649 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003650 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003651 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003652
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003653 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003654 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003655 try:
3656 revision = WaitForRealCommit(remote, revision, base_branch, branch)
3657 # We set pushed_to_pending to False, since it made it all the way to the
3658 # real ref.
3659 pushed_to_pending = False
3660 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003661 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003662
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003663 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003664 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003665 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003666 if not to_pending:
3667 if viewvc_url and revision:
3668 change_desc.append_footer(
3669 'Committed: %s%s' % (viewvc_url, revision))
3670 elif revision:
3671 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003672 print ('Closing issue '
3673 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003674 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003675 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003676 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00003677 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00003678 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00003679 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003680 if options.bypass_hooks:
3681 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
3682 else:
3683 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00003684 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003685 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003686
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003687 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003688 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
3689 print 'The commit is in the pending queue (%s).' % pending_ref
3690 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00003691 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003692 'footer.' % branch)
3693
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003694 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
3695 if os.path.isfile(hook):
3696 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003697
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003698 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003699
3700
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003701def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
3702 print
3703 print 'Waiting for commit to be landed on %s...' % real_ref
3704 print '(If you are impatient, you may Ctrl-C once without harm)'
3705 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
3706 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003707 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003708
3709 loop = 0
3710 while True:
3711 sys.stdout.write('fetching (%d)... \r' % loop)
3712 sys.stdout.flush()
3713 loop += 1
3714
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003715 if mirror:
3716 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003717 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
3718 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
3719 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
3720 for commit in commits.splitlines():
3721 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
3722 print 'Found commit on %s' % real_ref
3723 return commit
3724
3725 current_rev = to_rev
3726
3727
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003728def PushToGitPending(remote, pending_ref, upstream_ref):
3729 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
3730
3731 Returns:
3732 (retcode of last operation, output log of last operation).
3733 """
3734 assert pending_ref.startswith('refs/'), pending_ref
3735 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
3736 cherry = RunGit(['rev-parse', 'HEAD']).strip()
3737 code = 0
3738 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003739 max_attempts = 3
3740 attempts_left = max_attempts
3741 while attempts_left:
3742 if attempts_left != max_attempts:
3743 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
3744 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003745
3746 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003747 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003748 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003749 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003750 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003751 print 'Fetch failed with exit code %d.' % code
3752 if out.strip():
3753 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003754 continue
3755
3756 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003757 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003758 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003759 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003760 if code:
3761 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003762 'Your patch doesn\'t apply cleanly to ref \'%s\', '
3763 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003764 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
3765 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003766 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003767 return code, out
3768
3769 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003770 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003771 code, out = RunGitWithCode(
3772 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
3773 if code == 0:
3774 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003775 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003776 return code, out
3777
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003778 print 'Push failed with exit code %d.' % code
3779 if out.strip():
3780 print out.strip()
3781 if IsFatalPushFailure(out):
3782 print (
3783 'Fatal push error. Make sure your .netrc credentials and git '
3784 'user.email are correct and you have push access to the repo.')
3785 return code, out
3786
3787 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003788 return code, out
3789
3790
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003791def IsFatalPushFailure(push_stdout):
3792 """True if retrying push won't help."""
3793 return '(prohibited by Gerrit)' in push_stdout
3794
3795
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003796@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003797def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003798 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003799 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003800 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003801 # If it looks like previous commits were mirrored with git-svn.
3802 message = """This repository appears to be a git-svn mirror, but no
3803upstream SVN master is set. You probably need to run 'git auto-svn' once."""
3804 else:
3805 message = """This doesn't appear to be an SVN repository.
3806If your project has a true, writeable git repository, you probably want to run
3807'git cl land' instead.
3808If your project has a git mirror of an upstream SVN master, you probably need
3809to run 'git svn init'.
3810
3811Using the wrong command might cause your commit to appear to succeed, and the
3812review to be closed, without actually landing upstream. If you choose to
3813proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00003814 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00003815 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003816 return SendUpstream(parser, args, 'dcommit')
3817
3818
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003819@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003820def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003821 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003822 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003823 print('This appears to be an SVN repository.')
3824 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003825 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00003826 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003827 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003828
3829
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003830@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003831def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00003832 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003833 parser.add_option('-b', dest='newbranch',
3834 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003835 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003836 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003837 parser.add_option('-d', '--directory', action='store', metavar='DIR',
3838 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003839 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003840 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003841 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003842 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003843 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003844 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003845
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003846
3847 group = optparse.OptionGroup(
3848 parser,
3849 'Options for continuing work on the current issue uploaded from a '
3850 'different clone (e.g. different machine). Must be used independently '
3851 'from the other options. No issue number should be specified, and the '
3852 'branch must have an issue number associated with it')
3853 group.add_option('--reapply', action='store_true', dest='reapply',
3854 help='Reset the branch and reapply the issue.\n'
3855 'CAUTION: This will undo any local changes in this '
3856 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003857
3858 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003859 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003860 parser.add_option_group(group)
3861
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003862 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003863 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003864 auth_config = auth.extract_auth_config_from_options(options)
3865
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003866 cl = Changelist(auth_config=auth_config)
3867
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003868 issue_arg = None
3869 if options.reapply :
3870 if len(args) > 0:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003871 parser.error('--reapply implies no additional arguments.')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003872
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003873 issue_arg = cl.GetIssue()
3874 upstream = cl.GetUpstreamBranch()
3875 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003876 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003877
3878 RunGit(['reset', '--hard', upstream])
3879 if options.pull:
3880 RunGit(['pull'])
3881 else:
3882 if len(args) != 1:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003883 parser.error('Must specify issue number or url')
3884 issue_arg = args[0]
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003885
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003886 if not issue_arg:
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003887 parser.print_help()
3888 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003889
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003890 if cl.IsGerrit():
3891 if options.reject:
3892 parser.error('--reject is not supported with Gerrit codereview.')
3893 if options.nocommit:
3894 parser.error('--nocommit is not supported with Gerrit codereview.')
3895 if options.directory:
3896 parser.error('--directory is not supported with Gerrit codereview.')
3897
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003898 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003899 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003900 return 1
3901
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003902 if options.newbranch:
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003903 if options.reapply:
3904 parser.error("--reapply excludes any option other than --pull")
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003905 if options.force:
3906 RunGit(['branch', '-D', options.newbranch],
3907 stderr=subprocess2.PIPE, error_ok=True)
3908 RunGit(['checkout', '-b', options.newbranch,
3909 Changelist().GetUpstreamBranch()])
3910
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003911 return cl.CMDPatchIssue(issue_arg, options.reject, options.nocommit,
3912 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003913
3914
3915def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003916 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003917 # Provide a wrapper for git svn rebase to help avoid accidental
3918 # git svn dcommit.
3919 # It's the only command that doesn't use parser at all since we just defer
3920 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003921
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003922 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003923
3924
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003925def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003926 """Fetches the tree status and returns either 'open', 'closed',
3927 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003928 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003929 if url:
3930 status = urllib2.urlopen(url).read().lower()
3931 if status.find('closed') != -1 or status == '0':
3932 return 'closed'
3933 elif status.find('open') != -1 or status == '1':
3934 return 'open'
3935 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003936 return 'unset'
3937
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003938
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003939def GetTreeStatusReason():
3940 """Fetches the tree status from a json url and returns the message
3941 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00003942 url = settings.GetTreeStatusUrl()
3943 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003944 connection = urllib2.urlopen(json_url)
3945 status = json.loads(connection.read())
3946 connection.close()
3947 return status['message']
3948
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003949
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003950def GetBuilderMaster(bot_list):
3951 """For a given builder, fetch the master from AE if available."""
3952 map_url = 'https://builders-map.appspot.com/'
3953 try:
3954 master_map = json.load(urllib2.urlopen(map_url))
3955 except urllib2.URLError as e:
3956 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
3957 (map_url, e))
3958 except ValueError as e:
3959 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
3960 if not master_map:
3961 return None, 'Failed to build master map.'
3962
3963 result_master = ''
3964 for bot in bot_list:
3965 builder = bot.split(':', 1)[0]
3966 master_list = master_map.get(builder, [])
3967 if not master_list:
3968 return None, ('No matching master for builder %s.' % builder)
3969 elif len(master_list) > 1:
3970 return None, ('The builder name %s exists in multiple masters %s.' %
3971 (builder, master_list))
3972 else:
3973 cur_master = master_list[0]
3974 if not result_master:
3975 result_master = cur_master
3976 elif result_master != cur_master:
3977 return None, 'The builders do not belong to the same master.'
3978 return result_master, None
3979
3980
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003981def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003982 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003983 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003984 status = GetTreeStatus()
3985 if 'unset' == status:
3986 print 'You must configure your tree status URL by running "git cl config".'
3987 return 2
3988
3989 print "The tree is %s" % status
3990 print
3991 print GetTreeStatusReason()
3992 if status != 'open':
3993 return 1
3994 return 0
3995
3996
maruel@chromium.org15192402012-09-06 12:38:29 +00003997def CMDtry(parser, args):
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003998 """Triggers a try job through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00003999 group = optparse.OptionGroup(parser, "Try job options")
4000 group.add_option(
4001 "-b", "--bot", action="append",
4002 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
4003 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004004 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00004005 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004006 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00004007 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004008 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00004009 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004010 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004011 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004012 "-r", "--revision",
4013 help="Revision to use for the try job; default: the "
4014 "revision will be determined by the try server; see "
4015 "its waterfall for more info")
4016 group.add_option(
4017 "-c", "--clobber", action="store_true", default=False,
4018 help="Force a clobber before building; e.g. don't do an "
4019 "incremental build")
4020 group.add_option(
4021 "--project",
4022 help="Override which project to use. Projects are defined "
4023 "server-side to define what default bot set to use")
4024 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004025 "-p", "--property", dest="properties", action="append", default=[],
4026 help="Specify generic properties in the form -p key1=value1 -p "
4027 "key2=value2 etc (buildbucket only). The value will be treated as "
4028 "json if decodable, or as string otherwise.")
4029 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004030 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004031 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004032 "--use-rietveld", action="store_true", default=False,
4033 help="Use Rietveld to trigger try jobs.")
4034 group.add_option(
4035 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4036 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004037 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004038 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004039 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004040 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004041
machenbach@chromium.org45453142015-09-15 08:45:22 +00004042 if options.use_rietveld and options.properties:
4043 parser.error('Properties can only be specified with buildbucket')
4044
4045 # Make sure that all properties are prop=value pairs.
4046 bad_params = [x for x in options.properties if '=' not in x]
4047 if bad_params:
4048 parser.error('Got properties with missing "=": %s' % bad_params)
4049
maruel@chromium.org15192402012-09-06 12:38:29 +00004050 if args:
4051 parser.error('Unknown arguments: %s' % args)
4052
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004053 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004054 if not cl.GetIssue():
4055 parser.error('Need to upload first')
4056
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004057 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004058 if props.get('closed'):
4059 parser.error('Cannot send tryjobs for a closed CL')
4060
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004061 if props.get('private'):
4062 parser.error('Cannot use trybots with private issue')
4063
maruel@chromium.org15192402012-09-06 12:38:29 +00004064 if not options.name:
4065 options.name = cl.GetBranch()
4066
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004067 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004068 options.master, err_msg = GetBuilderMaster(options.bot)
4069 if err_msg:
4070 parser.error('Tryserver master cannot be found because: %s\n'
4071 'Please manually specify the tryserver master'
4072 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004073
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004074 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004075 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004076 if not options.bot:
4077 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004078
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004079 # Get try masters from PRESUBMIT.py files.
4080 masters = presubmit_support.DoGetTryMasters(
4081 change,
4082 change.LocalPaths(),
4083 settings.GetRoot(),
4084 None,
4085 None,
4086 options.verbose,
4087 sys.stdout)
4088 if masters:
4089 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004090
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004091 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4092 options.bot = presubmit_support.DoGetTrySlaves(
4093 change,
4094 change.LocalPaths(),
4095 settings.GetRoot(),
4096 None,
4097 None,
4098 options.verbose,
4099 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004100
4101 if not options.bot:
4102 # Get try masters from cq.cfg if any.
4103 # TODO(tandrii): some (but very few) projects store cq.cfg in different
4104 # location.
4105 cq_cfg = os.path.join(change.RepositoryRoot(),
4106 'infra', 'config', 'cq.cfg')
4107 if os.path.exists(cq_cfg):
4108 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00004109 cq_masters = commit_queue.get_master_builder_map(
4110 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004111 for master, builders in cq_masters.iteritems():
4112 for builder in builders:
4113 # Skip presubmit builders, because these will fail without LGTM.
4114 if 'presubmit' not in builder.lower():
4115 masters.setdefault(master, {})[builder] = ['defaulttests']
4116 if masters:
4117 return masters
4118
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004119 if not options.bot:
4120 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00004121
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004122 builders_and_tests = {}
4123 # TODO(machenbach): The old style command-line options don't support
4124 # multiple try masters yet.
4125 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4126 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4127
4128 for bot in old_style:
4129 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004130 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004131 elif ',' in bot:
4132 parser.error('Specify one bot per --bot flag')
4133 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004134 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004135
4136 for bot, tests in new_style:
4137 builders_and_tests.setdefault(bot, []).extend(tests)
4138
4139 # Return a master map with one master to be backwards compatible. The
4140 # master name defaults to an empty string, which will cause the master
4141 # not to be set on rietveld (deprecated).
4142 return {options.master: builders_and_tests}
4143
4144 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004145
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004146 for builders in masters.itervalues():
4147 if any('triggered' in b for b in builders):
4148 print >> sys.stderr, (
4149 'ERROR You are trying to send a job to a triggered bot. This type of'
4150 ' bot requires an\ninitial job from a parent (usually a builder). '
4151 'Instead send your job to the parent.\n'
4152 'Bot list: %s' % builders)
4153 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004154
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004155 patchset = cl.GetMostRecentPatchset()
4156 if patchset and patchset != cl.GetPatchset():
4157 print(
4158 '\nWARNING Mismatch between local config and server. Did a previous '
4159 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4160 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004161 if options.luci:
4162 trigger_luci_job(cl, masters, options)
4163 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004164 try:
4165 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4166 except BuildbucketResponseException as ex:
4167 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004168 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004169 except Exception as e:
4170 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4171 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
4172 e, stacktrace)
4173 return 1
4174 else:
4175 try:
4176 cl.RpcServer().trigger_distributed_try_jobs(
4177 cl.GetIssue(), patchset, options.name, options.clobber,
4178 options.revision, masters)
4179 except urllib2.HTTPError as e:
4180 if e.code == 404:
4181 print('404 from rietveld; '
4182 'did you mean to use "git try" instead of "git cl try"?')
4183 return 1
4184 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004185
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004186 for (master, builders) in sorted(masters.iteritems()):
4187 if master:
4188 print 'Master: %s' % master
4189 length = max(len(builder) for builder in builders)
4190 for builder in sorted(builders):
4191 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00004192 return 0
4193
4194
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004195def CMDtry_results(parser, args):
4196 group = optparse.OptionGroup(parser, "Try job results options")
4197 group.add_option(
4198 "-p", "--patchset", type=int, help="patchset number if not current.")
4199 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004200 "--print-master", action='store_true', help="print master name as well.")
4201 group.add_option(
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004202 "--color", action='store_true', default=setup_color.IS_TTY,
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004203 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004204 group.add_option(
4205 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4206 help="Host of buildbucket. The default host is %default.")
4207 parser.add_option_group(group)
4208 auth.add_auth_options(parser)
4209 options, args = parser.parse_args(args)
4210 if args:
4211 parser.error('Unrecognized args: %s' % ' '.join(args))
4212
4213 auth_config = auth.extract_auth_config_from_options(options)
4214 cl = Changelist(auth_config=auth_config)
4215 if not cl.GetIssue():
4216 parser.error('Need to upload first')
4217
4218 if not options.patchset:
4219 options.patchset = cl.GetMostRecentPatchset()
4220 if options.patchset and options.patchset != cl.GetPatchset():
4221 print(
4222 '\nWARNING Mismatch between local config and server. Did a previous '
4223 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4224 'Continuing using\npatchset %s.\n' % options.patchset)
4225 try:
4226 jobs = fetch_try_jobs(auth_config, cl, options)
4227 except BuildbucketResponseException as ex:
4228 print 'Buildbucket error: %s' % ex
4229 return 1
4230 except Exception as e:
4231 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4232 print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
4233 e, stacktrace)
4234 return 1
4235 print_tryjobs(options, jobs)
4236 return 0
4237
4238
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004239@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004240def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004241 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004242 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004243 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004244 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004245
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004246 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004247 if args:
4248 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004249 branch = cl.GetBranch()
4250 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004251 cl = Changelist()
4252 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004253
4254 # Clear configured merge-base, if there is one.
4255 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004256 else:
4257 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004258 return 0
4259
4260
thestig@chromium.org00858c82013-12-02 23:08:03 +00004261def CMDweb(parser, args):
4262 """Opens the current CL in the web browser."""
4263 _, args = parser.parse_args(args)
4264 if args:
4265 parser.error('Unrecognized args: %s' % ' '.join(args))
4266
4267 issue_url = Changelist().GetIssueURL()
4268 if not issue_url:
4269 print >> sys.stderr, 'ERROR No issue to open'
4270 return 1
4271
4272 webbrowser.open(issue_url)
4273 return 0
4274
4275
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004276def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004277 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004278 auth.add_auth_options(parser)
4279 options, args = parser.parse_args(args)
4280 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004281 if args:
4282 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004283 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004284 props = cl.GetIssueProperties()
4285 if props.get('private'):
4286 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004287 cl.SetFlag('commit', '1')
4288 return 0
4289
4290
groby@chromium.org411034a2013-02-26 15:12:01 +00004291def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004292 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004293 auth.add_auth_options(parser)
4294 options, args = parser.parse_args(args)
4295 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004296 if args:
4297 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004298 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00004299 # Ensure there actually is an issue to close.
4300 cl.GetDescription()
4301 cl.CloseIssue()
4302 return 0
4303
4304
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004305def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004306 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004307 auth.add_auth_options(parser)
4308 options, args = parser.parse_args(args)
4309 auth_config = auth.extract_auth_config_from_options(options)
4310 if args:
4311 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004312
4313 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004314 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004315 # Staged changes would be committed along with the patch from last
4316 # upload, hence counted toward the "last upload" side in the final
4317 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004318 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004319 return 1
4320
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004321 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004322 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004323 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004324 if not issue:
4325 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004326 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004327 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004328
4329 # Create a new branch based on the merge-base
4330 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004331 # Clear cached branch in cl object, to avoid overwriting original CL branch
4332 # properties.
4333 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004334 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004335 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004336 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004337 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004338 return rtn
4339
wychen@chromium.org06928532015-02-03 02:11:29 +00004340 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004341 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00004342 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004343 finally:
4344 RunGit(['checkout', '-q', branch])
4345 RunGit(['branch', '-D', TMP_BRANCH])
4346
4347 return 0
4348
4349
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004350def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004351 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004352 parser.add_option(
4353 '--no-color',
4354 action='store_true',
4355 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004356 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004357 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004358 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004359
4360 author = RunGit(['config', 'user.email']).strip() or None
4361
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004362 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004363
4364 if args:
4365 if len(args) > 1:
4366 parser.error('Unknown args')
4367 base_branch = args[0]
4368 else:
4369 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004370 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004371
4372 change = cl.GetChange(base_branch, None)
4373 return owners_finder.OwnersFinder(
4374 [f.LocalPath() for f in
4375 cl.GetChange(base_branch, None).AffectedFiles()],
4376 change.RepositoryRoot(), author,
4377 fopen=file, os_path=os.path, glob=glob.glob,
4378 disable_color=options.no_color).run()
4379
4380
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004381def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004382 """Generates a diff command."""
4383 # Generate diff for the current branch's changes.
4384 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4385 upstream_commit, '--' ]
4386
4387 if args:
4388 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004389 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004390 diff_cmd.append(arg)
4391 else:
4392 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004393
4394 return diff_cmd
4395
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004396def MatchingFileType(file_name, extensions):
4397 """Returns true if the file name ends with one of the given extensions."""
4398 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004399
enne@chromium.org555cfe42014-01-29 18:21:39 +00004400@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004401def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004402 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00004403 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004404 GN_EXTS = ['.gn', '.gni']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004405 parser.add_option('--full', action='store_true',
4406 help='Reformat the full content of all touched files')
4407 parser.add_option('--dry-run', action='store_true',
4408 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004409 parser.add_option('--python', action='store_true',
4410 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004411 parser.add_option('--diff', action='store_true',
4412 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004413 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004414
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004415 # git diff generates paths against the root of the repository. Change
4416 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004417 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004418 if rel_base_path:
4419 os.chdir(rel_base_path)
4420
digit@chromium.org29e47272013-05-17 17:01:46 +00004421 # Grab the merge-base commit, i.e. the upstream commit of the current
4422 # branch when it was created or the last time it was rebased. This is
4423 # to cover the case where the user may have called "git fetch origin",
4424 # moving the origin branch to a newer commit, but hasn't rebased yet.
4425 upstream_commit = None
4426 cl = Changelist()
4427 upstream_branch = cl.GetUpstreamBranch()
4428 if upstream_branch:
4429 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4430 upstream_commit = upstream_commit.strip()
4431
4432 if not upstream_commit:
4433 DieWithError('Could not find base commit for this branch. '
4434 'Are you in detached state?')
4435
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004436 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4437 diff_output = RunGit(changed_files_cmd)
4438 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004439 # Filter out files deleted by this CL
4440 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004441
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004442 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4443 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4444 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004445 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004446
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004447 top_dir = os.path.normpath(
4448 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
4449
4450 # Locate the clang-format binary in the checkout
4451 try:
4452 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
4453 except clang_format.NotFoundError, e:
4454 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00004455
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004456 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4457 # formatted. This is used to block during the presubmit.
4458 return_value = 0
4459
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004460 if clang_diff_files:
4461 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004462 cmd = [clang_format_tool]
4463 if not opts.dry_run and not opts.diff:
4464 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004465 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004466 if opts.diff:
4467 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004468 else:
4469 env = os.environ.copy()
4470 env['PATH'] = str(os.path.dirname(clang_format_tool))
4471 try:
4472 script = clang_format.FindClangFormatScriptInChromiumTree(
4473 'clang-format-diff.py')
4474 except clang_format.NotFoundError, e:
4475 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004476
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004477 cmd = [sys.executable, script, '-p0']
4478 if not opts.dry_run and not opts.diff:
4479 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004480
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004481 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
4482 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004483
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004484 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
4485 if opts.diff:
4486 sys.stdout.write(stdout)
4487 if opts.dry_run and len(stdout) > 0:
4488 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004489
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004490 # Similar code to above, but using yapf on .py files rather than clang-format
4491 # on C/C++ files
4492 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004493 yapf_tool = gclient_utils.FindExecutable('yapf')
4494 if yapf_tool is None:
4495 DieWithError('yapf not found in PATH')
4496
4497 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004498 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004499 cmd = [yapf_tool]
4500 if not opts.dry_run and not opts.diff:
4501 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004502 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004503 if opts.diff:
4504 sys.stdout.write(stdout)
4505 else:
4506 # TODO(sbc): yapf --lines mode still has some issues.
4507 # https://github.com/google/yapf/issues/154
4508 DieWithError('--python currently only works with --full')
4509
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004510 # Dart's formatter does not have the nice property of only operating on
4511 # modified chunks, so hard code full.
4512 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004513 try:
4514 command = [dart_format.FindDartFmtToolInChromiumTree()]
4515 if not opts.dry_run and not opts.diff:
4516 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004517 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004518
ppi@chromium.org6593d932016-03-03 15:41:15 +00004519 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004520 if opts.dry_run and stdout:
4521 return_value = 2
4522 except dart_format.NotFoundError as e:
erikcorry@chromium.org3e445022015-12-17 09:07:26 +00004523 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
4524 'found in this checkout. Files in other languages are still ' +
4525 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004526
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004527 # Format GN build files. Always run on full build files for canonical form.
4528 if gn_diff_files:
4529 cmd = ['gn', 'format']
4530 if not opts.dry_run and not opts.diff:
4531 cmd.append('--in-place')
4532 for gn_diff_file in gn_diff_files:
4533 stdout = RunCommand(cmd + [gn_diff_file], cwd=top_dir)
4534 if opts.diff:
4535 sys.stdout.write(stdout)
4536
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004537 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004538
4539
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004540@subcommand.usage('<codereview url or issue id>')
4541def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004542 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004543 _, args = parser.parse_args(args)
4544
4545 if len(args) != 1:
4546 parser.print_help()
4547 return 1
4548
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004549 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00004550 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004551 parser.print_help()
4552 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00004553 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004554
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004555 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00004556 output = RunGit(['config', '--local', '--get-regexp',
4557 r'branch\..*\.%s' % issueprefix],
4558 error_ok=True)
4559 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004560 if issue == target_issue:
4561 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004562
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004563 branches = []
4564 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
4565 branches.extend(find_issues(cls.IssueSettingPrefix()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004566 if len(branches) == 0:
4567 print 'No branch found for issue %s.' % target_issue
4568 return 1
4569 if len(branches) == 1:
4570 RunGit(['checkout', branches[0]])
4571 else:
4572 print 'Multiple branches match issue %s:' % target_issue
4573 for i in range(len(branches)):
4574 print '%d: %s' % (i, branches[i])
4575 which = raw_input('Choose by index: ')
4576 try:
4577 RunGit(['checkout', branches[int(which)]])
4578 except (IndexError, ValueError):
4579 print 'Invalid selection, not checking out any branch.'
4580 return 1
4581
4582 return 0
4583
4584
maruel@chromium.org29404b52014-09-08 22:58:00 +00004585def CMDlol(parser, args):
4586 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00004587 print zlib.decompress(base64.b64decode(
4588 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
4589 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
4590 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
4591 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00004592 return 0
4593
4594
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004595class OptionParser(optparse.OptionParser):
4596 """Creates the option parse and add --verbose support."""
4597 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004598 optparse.OptionParser.__init__(
4599 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004600 self.add_option(
4601 '-v', '--verbose', action='count', default=0,
4602 help='Use 2 times for more debugging info')
4603
4604 def parse_args(self, args=None, values=None):
4605 options, args = optparse.OptionParser.parse_args(self, args, values)
4606 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
4607 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
4608 return options, args
4609
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004610
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004611def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00004612 if sys.hexversion < 0x02060000:
4613 print >> sys.stderr, (
4614 '\nYour python version %s is unsupported, please upgrade.\n' %
4615 sys.version.split(' ', 1)[0])
4616 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004617
maruel@chromium.orgddd59412011-11-30 14:20:38 +00004618 # Reload settings.
4619 global settings
4620 settings = Settings()
4621
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004622 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004623 dispatcher = subcommand.CommandDispatcher(__name__)
4624 try:
4625 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00004626 except auth.AuthenticationError as e:
4627 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004628 except urllib2.HTTPError, e:
4629 if e.code != 500:
4630 raise
4631 DieWithError(
4632 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
4633 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00004634 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004635
4636
4637if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004638 # These affect sys.stdout so do it outside of main() to simplify mocks in
4639 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00004640 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004641 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00004642 try:
4643 sys.exit(main(sys.argv[1:]))
4644 except KeyboardInterrupt:
4645 sys.stderr.write('interrupted\n')
4646 sys.exit(1)