blob: f3606bfc32fbd48d9c7f1ffcd8f319a115901000 [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
maruel@chromium.org725f1c32011-04-01 20:24:54 +00008"""A git-command for integrating reviews on Rietveld."""
9
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000010from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000011from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000012import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000013import collections
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000014import glob
sheyang@google.com6ebaf782015-05-12 19:17:54 +000015import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000016import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import logging
18import optparse
19import os
maruel@chromium.org1033efd2013-07-23 23:25:09 +000020import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000022import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000023import sys
bauerb@chromium.org27386dd2015-02-16 10:45:39 +000024import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025import textwrap
sheyang@google.com6ebaf782015-05-12 19:17:54 +000026import time
27import traceback
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000028import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000030import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000031import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000032import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000033import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000034
35try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000036 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000037except ImportError:
38 pass
39
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000040from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000041from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000043import auth
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +000044from luci_hacks import trigger_luci_job as luci_trigger
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000045import clang_format
tandrii@chromium.org71184c02016-01-13 15:18:44 +000046import commit_queue
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000047import dart_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000048import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000049import gclient_utils
szager@chromium.org151ebcf2016-03-09 01:08:25 +000050import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000051import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000052import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000053import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000054import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000055import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000056import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000057import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000058import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000059import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000060import watchlists
61
maruel@chromium.org0633fb42013-08-16 20:06:14 +000062__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000063
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000064DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000065POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000066DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000067GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
rmistry@google.comc68112d2015-03-03 12:48:06 +000068REFS_THAT_ALIAS_TO_OTHER_REFS = {
69 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
70 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
71}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000072
thestig@chromium.org44202a22014-03-11 19:22:18 +000073# Valid extensions for files we want to lint.
74DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
75DEFAULT_LINT_IGNORE_REGEX = r"$^"
76
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000077# Shortcut since it quickly becomes redundant.
78Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000079
maruel@chromium.orgddd59412011-11-30 14:20:38 +000080# Initialized in main()
81settings = None
82
83
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000084def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000085 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000086 sys.exit(1)
87
88
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000089def GetNoGitPagerEnv():
90 env = os.environ.copy()
91 # 'cat' is a magical git string that disables pagers on all platforms.
92 env['GIT_PAGER'] = 'cat'
93 return env
94
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000095
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000096def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000097 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000098 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000099 except subprocess2.CalledProcessError as e:
100 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000101 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000102 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000103 'Command "%s" failed.\n%s' % (
104 ' '.join(args), error_message or e.stdout or ''))
105 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000106
107
108def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000109 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000110 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000111
112
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000113def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000114 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000115 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000116 if suppress_stderr:
117 stderr = subprocess2.VOID
118 else:
119 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000120 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000121 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000122 stdout=subprocess2.PIPE,
123 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000124 return code, out[0]
125 except ValueError:
126 # When the subprocess fails, it returns None. That triggers a ValueError
127 # when trying to unpack the return value into (out, code).
128 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000129
130
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000131def RunGitSilent(args):
132 """Returns stdout, suppresses stderr and ingores the return code."""
133 return RunGitWithCode(args, suppress_stderr=True)[1]
134
135
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000136def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000137 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000138 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000139 return (version.startswith(prefix) and
140 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000141
142
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000143def BranchExists(branch):
144 """Return True if specified branch exists."""
145 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
146 suppress_stderr=True)
147 return not code
148
149
maruel@chromium.org90541732011-04-01 17:54:18 +0000150def ask_for_data(prompt):
151 try:
152 return raw_input(prompt)
153 except KeyboardInterrupt:
154 # Hide the exception.
155 sys.exit(1)
156
157
iannucci@chromium.org79540052012-10-19 23:15:26 +0000158def git_set_branch_value(key, value):
159 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000160 if not branch:
161 return
162
163 cmd = ['config']
164 if isinstance(value, int):
165 cmd.append('--int')
166 git_key = 'branch.%s.%s' % (branch, key)
167 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000168
169
170def git_get_branch_default(key, default):
171 branch = Changelist().GetBranch()
172 if branch:
173 git_key = 'branch.%s.%s' % (branch, key)
174 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
175 try:
176 return int(stdout.strip())
177 except ValueError:
178 pass
179 return default
180
181
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000182def add_git_similarity(parser):
183 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000184 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000185 help='Sets the percentage that a pair of files need to match in order to'
186 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000187 parser.add_option(
188 '--find-copies', action='store_true',
189 help='Allows git to look for copies.')
190 parser.add_option(
191 '--no-find-copies', action='store_false', dest='find_copies',
192 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000193
194 old_parser_args = parser.parse_args
195 def Parse(args):
196 options, args = old_parser_args(args)
197
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000198 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000199 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000200 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000201 print('Note: Saving similarity of %d%% in git config.'
202 % options.similarity)
203 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000204
iannucci@chromium.org79540052012-10-19 23:15:26 +0000205 options.similarity = max(0, min(options.similarity, 100))
206
207 if options.find_copies is None:
208 options.find_copies = bool(
209 git_get_branch_default('git-find-copies', True))
210 else:
211 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000212
213 print('Using %d%% similarity for rename/copy detection. '
214 'Override with --similarity.' % options.similarity)
215
216 return options, args
217 parser.parse_args = Parse
218
219
machenbach@chromium.org45453142015-09-15 08:45:22 +0000220def _get_properties_from_options(options):
221 properties = dict(x.split('=', 1) for x in options.properties)
222 for key, val in properties.iteritems():
223 try:
224 properties[key] = json.loads(val)
225 except ValueError:
226 pass # If a value couldn't be evaluated, treat it as a string.
227 return properties
228
229
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000230def _prefix_master(master):
231 """Convert user-specified master name to full master name.
232
233 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
234 name, while the developers always use shortened master name
235 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
236 function does the conversion for buildbucket migration.
237 """
238 prefix = 'master.'
239 if master.startswith(prefix):
240 return master
241 return '%s%s' % (prefix, master)
242
243
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000244def _buildbucket_retry(operation_name, http, *args, **kwargs):
245 """Retries requests to buildbucket service and returns parsed json content."""
246 try_count = 0
247 while True:
248 response, content = http.request(*args, **kwargs)
249 try:
250 content_json = json.loads(content)
251 except ValueError:
252 content_json = None
253
254 # Buildbucket could return an error even if status==200.
255 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000256 error = content_json.get('error')
257 if error.get('code') == 403:
258 raise BuildbucketResponseException(
259 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000260 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000261 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000262 raise BuildbucketResponseException(msg)
263
264 if response.status == 200:
265 if not content_json:
266 raise BuildbucketResponseException(
267 'Buildbucket returns invalid json content: %s.\n'
268 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
269 content)
270 return content_json
271 if response.status < 500 or try_count >= 2:
272 raise httplib2.HttpLib2Error(content)
273
274 # status >= 500 means transient failures.
275 logging.debug('Transient errors when %s. Will retry.', operation_name)
276 time.sleep(0.5 + 1.5*try_count)
277 try_count += 1
278 assert False, 'unreachable'
279
280
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000281def trigger_luci_job(changelist, masters, options):
282 """Send a job to run on LUCI."""
283 issue_props = changelist.GetIssueProperties()
284 issue = changelist.GetIssue()
285 patchset = changelist.GetMostRecentPatchset()
286 for builders_and_tests in sorted(masters.itervalues()):
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000287 # TODO(hinoka et al): add support for other properties.
288 # Currently, this completely ignores testfilter and other properties.
289 for builder in sorted(builders_and_tests):
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000290 luci_trigger.trigger(
291 builder, 'HEAD', issue, patchset, issue_props['project'])
292
293
machenbach@chromium.org45453142015-09-15 08:45:22 +0000294def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000295 rietveld_url = settings.GetDefaultServerUrl()
296 rietveld_host = urlparse.urlparse(rietveld_url).hostname
297 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
298 http = authenticator.authorize(httplib2.Http())
299 http.force_exception_to_status_code = True
300 issue_props = changelist.GetIssueProperties()
301 issue = changelist.GetIssue()
302 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000303 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000304
305 buildbucket_put_url = (
306 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000307 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000308 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
309 hostname=rietveld_host,
310 issue=issue,
311 patch=patchset)
312
313 batch_req_body = {'builds': []}
314 print_text = []
315 print_text.append('Tried jobs on:')
316 for master, builders_and_tests in sorted(masters.iteritems()):
317 print_text.append('Master: %s' % master)
318 bucket = _prefix_master(master)
319 for builder, tests in sorted(builders_and_tests.iteritems()):
320 print_text.append(' %s: %s' % (builder, tests))
321 parameters = {
322 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000323 'changes': [{
324 'author': {'email': issue_props['owner_email']},
325 'revision': options.revision,
326 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000327 'properties': {
328 'category': category,
329 'issue': issue,
330 'master': master,
331 'patch_project': issue_props['project'],
332 'patch_storage': 'rietveld',
333 'patchset': patchset,
334 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000335 'rietveld': rietveld_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000336 },
337 }
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000338 if tests:
339 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000340 if properties:
341 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000342 if options.clobber:
343 parameters['properties']['clobber'] = True
344 batch_req_body['builds'].append(
345 {
346 'bucket': bucket,
347 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000348 'client_operation_id': str(uuid.uuid4()),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000349 'tags': ['builder:%s' % builder,
350 'buildset:%s' % buildset,
351 'master:%s' % master,
352 'user_agent:git_cl_try']
353 }
354 )
355
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000356 _buildbucket_retry(
357 'triggering tryjobs',
358 http,
359 buildbucket_put_url,
360 'PUT',
361 body=json.dumps(batch_req_body),
362 headers={'Content-Type': 'application/json'}
363 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000364 print_text.append('To see results here, run: git cl try-results')
365 print_text.append('To see results in browser, run: git cl web')
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000366 print '\n'.join(print_text)
kjellander@chromium.org44424542015-06-02 18:35:29 +0000367
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000368
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000369def fetch_try_jobs(auth_config, changelist, options):
370 """Fetches tryjobs from buildbucket.
371
372 Returns a map from build id to build info as json dictionary.
373 """
374 rietveld_url = settings.GetDefaultServerUrl()
375 rietveld_host = urlparse.urlparse(rietveld_url).hostname
376 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
377 if authenticator.has_cached_credentials():
378 http = authenticator.authorize(httplib2.Http())
379 else:
380 print ('Warning: Some results might be missing because %s' %
381 # Get the message on how to login.
382 auth.LoginRequiredError(rietveld_host).message)
383 http = httplib2.Http()
384
385 http.force_exception_to_status_code = True
386
387 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
388 hostname=rietveld_host,
389 issue=changelist.GetIssue(),
390 patch=options.patchset)
391 params = {'tag': 'buildset:%s' % buildset}
392
393 builds = {}
394 while True:
395 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
396 hostname=options.buildbucket_host,
397 params=urllib.urlencode(params))
398 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
399 for build in content.get('builds', []):
400 builds[build['id']] = build
401 if 'next_cursor' in content:
402 params['start_cursor'] = content['next_cursor']
403 else:
404 break
405 return builds
406
407
408def print_tryjobs(options, builds):
409 """Prints nicely result of fetch_try_jobs."""
410 if not builds:
411 print 'No tryjobs scheduled'
412 return
413
414 # Make a copy, because we'll be modifying builds dictionary.
415 builds = builds.copy()
416 builder_names_cache = {}
417
418 def get_builder(b):
419 try:
420 return builder_names_cache[b['id']]
421 except KeyError:
422 try:
423 parameters = json.loads(b['parameters_json'])
424 name = parameters['builder_name']
425 except (ValueError, KeyError) as error:
426 print 'WARNING: failed to get builder name for build %s: %s' % (
427 b['id'], error)
428 name = None
429 builder_names_cache[b['id']] = name
430 return name
431
432 def get_bucket(b):
433 bucket = b['bucket']
434 if bucket.startswith('master.'):
435 return bucket[len('master.'):]
436 return bucket
437
438 if options.print_master:
439 name_fmt = '%%-%ds %%-%ds' % (
440 max(len(str(get_bucket(b))) for b in builds.itervalues()),
441 max(len(str(get_builder(b))) for b in builds.itervalues()))
442 def get_name(b):
443 return name_fmt % (get_bucket(b), get_builder(b))
444 else:
445 name_fmt = '%%-%ds' % (
446 max(len(str(get_builder(b))) for b in builds.itervalues()))
447 def get_name(b):
448 return name_fmt % get_builder(b)
449
450 def sort_key(b):
451 return b['status'], b.get('result'), get_name(b), b.get('url')
452
453 def pop(title, f, color=None, **kwargs):
454 """Pop matching builds from `builds` dict and print them."""
455
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000456 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000457 colorize = str
458 else:
459 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
460
461 result = []
462 for b in builds.values():
463 if all(b.get(k) == v for k, v in kwargs.iteritems()):
464 builds.pop(b['id'])
465 result.append(b)
466 if result:
467 print colorize(title)
468 for b in sorted(result, key=sort_key):
469 print ' ', colorize('\t'.join(map(str, f(b))))
470
471 total = len(builds)
472 pop(status='COMPLETED', result='SUCCESS',
473 title='Successes:', color=Fore.GREEN,
474 f=lambda b: (get_name(b), b.get('url')))
475 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
476 title='Infra Failures:', color=Fore.MAGENTA,
477 f=lambda b: (get_name(b), b.get('url')))
478 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
479 title='Failures:', color=Fore.RED,
480 f=lambda b: (get_name(b), b.get('url')))
481 pop(status='COMPLETED', result='CANCELED',
482 title='Canceled:', color=Fore.MAGENTA,
483 f=lambda b: (get_name(b),))
484 pop(status='COMPLETED', result='FAILURE',
485 failure_reason='INVALID_BUILD_DEFINITION',
486 title='Wrong master/builder name:', color=Fore.MAGENTA,
487 f=lambda b: (get_name(b),))
488 pop(status='COMPLETED', result='FAILURE',
489 title='Other failures:',
490 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
491 pop(status='COMPLETED',
492 title='Other finished:',
493 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
494 pop(status='STARTED',
495 title='Started:', color=Fore.YELLOW,
496 f=lambda b: (get_name(b), b.get('url')))
497 pop(status='SCHEDULED',
498 title='Scheduled:',
499 f=lambda b: (get_name(b), 'id=%s' % b['id']))
500 # The last section is just in case buildbucket API changes OR there is a bug.
501 pop(title='Other:',
502 f=lambda b: (get_name(b), 'id=%s' % b['id']))
503 assert len(builds) == 0
504 print 'Total: %d tryjobs' % total
505
506
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000507def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
508 """Return the corresponding git ref if |base_url| together with |glob_spec|
509 matches the full |url|.
510
511 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
512 """
513 fetch_suburl, as_ref = glob_spec.split(':')
514 if allow_wildcards:
515 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
516 if glob_match:
517 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
518 # "branches/{472,597,648}/src:refs/remotes/svn/*".
519 branch_re = re.escape(base_url)
520 if glob_match.group(1):
521 branch_re += '/' + re.escape(glob_match.group(1))
522 wildcard = glob_match.group(2)
523 if wildcard == '*':
524 branch_re += '([^/]*)'
525 else:
526 # Escape and replace surrounding braces with parentheses and commas
527 # with pipe symbols.
528 wildcard = re.escape(wildcard)
529 wildcard = re.sub('^\\\\{', '(', wildcard)
530 wildcard = re.sub('\\\\,', '|', wildcard)
531 wildcard = re.sub('\\\\}$', ')', wildcard)
532 branch_re += wildcard
533 if glob_match.group(3):
534 branch_re += re.escape(glob_match.group(3))
535 match = re.match(branch_re, url)
536 if match:
537 return re.sub('\*$', match.group(1), as_ref)
538
539 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
540 if fetch_suburl:
541 full_url = base_url + '/' + fetch_suburl
542 else:
543 full_url = base_url
544 if full_url == url:
545 return as_ref
546 return None
547
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000548
iannucci@chromium.org79540052012-10-19 23:15:26 +0000549def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000550 """Prints statistics about the change to the user."""
551 # --no-ext-diff is broken in some versions of Git, so try to work around
552 # this by overriding the environment (but there is still a problem if the
553 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000554 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000555 if 'GIT_EXTERNAL_DIFF' in env:
556 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000557
558 if find_copies:
559 similarity_options = ['--find-copies-harder', '-l100000',
560 '-C%s' % similarity]
561 else:
562 similarity_options = ['-M%s' % similarity]
563
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000564 try:
565 stdout = sys.stdout.fileno()
566 except AttributeError:
567 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000568 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000569 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000570 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000571 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000572
573
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000574class BuildbucketResponseException(Exception):
575 pass
576
577
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000578class Settings(object):
579 def __init__(self):
580 self.default_server = None
581 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000582 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000583 self.is_git_svn = None
584 self.svn_branch = None
585 self.tree_status_url = None
586 self.viewvc_url = None
587 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000588 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000589 self.squash_gerrit_uploads = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000590 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000591 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000592 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000593 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000594
595 def LazyUpdateIfNeeded(self):
596 """Updates the settings from a codereview.settings file, if available."""
597 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000598 # The only value that actually changes the behavior is
599 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000600 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000601 error_ok=True
602 ).strip().lower()
603
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000604 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000605 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000606 LoadCodereviewSettingsFromFile(cr_settings_file)
607 self.updated = True
608
609 def GetDefaultServerUrl(self, error_ok=False):
610 if not self.default_server:
611 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000612 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000613 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000614 if error_ok:
615 return self.default_server
616 if not self.default_server:
617 error_message = ('Could not find settings file. You must configure '
618 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000619 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000620 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000621 return self.default_server
622
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000623 @staticmethod
624 def GetRelativeRoot():
625 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000626
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000627 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000628 if self.root is None:
629 self.root = os.path.abspath(self.GetRelativeRoot())
630 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000631
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000632 def GetGitMirror(self, remote='origin'):
633 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000634 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000635 if not os.path.isdir(local_url):
636 return None
637 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
638 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
639 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
640 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
641 if mirror.exists():
642 return mirror
643 return None
644
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000645 def GetIsGitSvn(self):
646 """Return true if this repo looks like it's using git-svn."""
647 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000648 if self.GetPendingRefPrefix():
649 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
650 self.is_git_svn = False
651 else:
652 # If you have any "svn-remote.*" config keys, we think you're using svn.
653 self.is_git_svn = RunGitWithCode(
654 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000655 return self.is_git_svn
656
657 def GetSVNBranch(self):
658 if self.svn_branch is None:
659 if not self.GetIsGitSvn():
660 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
661
662 # Try to figure out which remote branch we're based on.
663 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000664 # 1) iterate through our branch history and find the svn URL.
665 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000666
667 # regexp matching the git-svn line that contains the URL.
668 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
669
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000670 # We don't want to go through all of history, so read a line from the
671 # pipe at a time.
672 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000673 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000674 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
675 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000676 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000677 for line in proc.stdout:
678 match = git_svn_re.match(line)
679 if match:
680 url = match.group(1)
681 proc.stdout.close() # Cut pipe.
682 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000683
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000684 if url:
685 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
686 remotes = RunGit(['config', '--get-regexp',
687 r'^svn-remote\..*\.url']).splitlines()
688 for remote in remotes:
689 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000690 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000691 remote = match.group(1)
692 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000693 rewrite_root = RunGit(
694 ['config', 'svn-remote.%s.rewriteRoot' % remote],
695 error_ok=True).strip()
696 if rewrite_root:
697 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000698 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000699 ['config', 'svn-remote.%s.fetch' % remote],
700 error_ok=True).strip()
701 if fetch_spec:
702 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
703 if self.svn_branch:
704 break
705 branch_spec = RunGit(
706 ['config', 'svn-remote.%s.branches' % remote],
707 error_ok=True).strip()
708 if branch_spec:
709 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
710 if self.svn_branch:
711 break
712 tag_spec = RunGit(
713 ['config', 'svn-remote.%s.tags' % remote],
714 error_ok=True).strip()
715 if tag_spec:
716 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
717 if self.svn_branch:
718 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000719
720 if not self.svn_branch:
721 DieWithError('Can\'t guess svn branch -- try specifying it on the '
722 'command line')
723
724 return self.svn_branch
725
726 def GetTreeStatusUrl(self, error_ok=False):
727 if not self.tree_status_url:
728 error_message = ('You must configure your tree status URL by running '
729 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000730 self.tree_status_url = self._GetRietveldConfig(
731 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000732 return self.tree_status_url
733
734 def GetViewVCUrl(self):
735 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000736 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000737 return self.viewvc_url
738
rmistry@google.com90752582014-01-14 21:04:50 +0000739 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000740 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000741
rmistry@google.com78948ed2015-07-08 23:09:57 +0000742 def GetIsSkipDependencyUpload(self, branch_name):
743 """Returns true if specified branch should skip dep uploads."""
744 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
745 error_ok=True)
746
rmistry@google.com5626a922015-02-26 14:03:30 +0000747 def GetRunPostUploadHook(self):
748 run_post_upload_hook = self._GetRietveldConfig(
749 'run-post-upload-hook', error_ok=True)
750 return run_post_upload_hook == "True"
751
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000752 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000753 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000754
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000755 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000756 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000757
ukai@chromium.orge8077812012-02-03 03:41:46 +0000758 def GetIsGerrit(self):
759 """Return true if this repo is assosiated with gerrit code review system."""
760 if self.is_gerrit is None:
761 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
762 return self.is_gerrit
763
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000764 def GetSquashGerritUploads(self):
765 """Return true if uploads to Gerrit should be squashed by default."""
766 if self.squash_gerrit_uploads is None:
767 self.squash_gerrit_uploads = (
768 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
769 error_ok=True).strip() == 'true')
770 return self.squash_gerrit_uploads
771
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000772 def GetGitEditor(self):
773 """Return the editor specified in the git config, or None if none is."""
774 if self.git_editor is None:
775 self.git_editor = self._GetConfig('core.editor', error_ok=True)
776 return self.git_editor or None
777
thestig@chromium.org44202a22014-03-11 19:22:18 +0000778 def GetLintRegex(self):
779 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
780 DEFAULT_LINT_REGEX)
781
782 def GetLintIgnoreRegex(self):
783 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
784 DEFAULT_LINT_IGNORE_REGEX)
785
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000786 def GetProject(self):
787 if not self.project:
788 self.project = self._GetRietveldConfig('project', error_ok=True)
789 return self.project
790
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000791 def GetForceHttpsCommitUrl(self):
792 if not self.force_https_commit_url:
793 self.force_https_commit_url = self._GetRietveldConfig(
794 'force-https-commit-url', error_ok=True)
795 return self.force_https_commit_url
796
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000797 def GetPendingRefPrefix(self):
798 if not self.pending_ref_prefix:
799 self.pending_ref_prefix = self._GetRietveldConfig(
800 'pending-ref-prefix', error_ok=True)
801 return self.pending_ref_prefix
802
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000803 def _GetRietveldConfig(self, param, **kwargs):
804 return self._GetConfig('rietveld.' + param, **kwargs)
805
rmistry@google.com78948ed2015-07-08 23:09:57 +0000806 def _GetBranchConfig(self, branch_name, param, **kwargs):
807 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
808
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000809 def _GetConfig(self, param, **kwargs):
810 self.LazyUpdateIfNeeded()
811 return RunGit(['config', param], **kwargs).strip()
812
813
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000814def ShortBranchName(branch):
815 """Convert a name like 'refs/heads/foo' to just 'foo'."""
816 return branch.replace('refs/heads/', '')
817
818
819class Changelist(object):
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000820 def __init__(self, branchref=None, issue=None, auth_config=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000821 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000822 global settings
823 if not settings:
824 # Happens when git_cl.py is used as a utility library.
825 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000826 settings.GetDefaultServerUrl()
827 self.branchref = branchref
828 if self.branchref:
829 self.branch = ShortBranchName(self.branchref)
830 else:
831 self.branch = None
832 self.rietveld_server = None
833 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000834 self.lookedup_issue = False
835 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000836 self.has_description = False
837 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000838 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000839 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000840 self.cc = None
841 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000842 self._auth_config = auth_config
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000843 self._props = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000844 self._remote = None
845 self._rpc_server = None
846
847 @property
848 def auth_config(self):
849 return self._auth_config
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000850
851 def GetCCList(self):
852 """Return the users cc'd on this CL.
853
854 Return is a string suitable for passing to gcl with the --cc flag.
855 """
856 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000857 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000858 more_cc = ','.join(self.watchers)
859 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
860 return self.cc
861
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000862 def GetCCListWithoutDefault(self):
863 """Return the users cc'd on this CL excluding default ones."""
864 if self.cc is None:
865 self.cc = ','.join(self.watchers)
866 return self.cc
867
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000868 def SetWatchers(self, watchers):
869 """Set the list of email addresses that should be cc'd based on the changed
870 files in this CL.
871 """
872 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000873
874 def GetBranch(self):
875 """Returns the short branch name, e.g. 'master'."""
876 if not self.branch:
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000877 branchref = RunGit(['symbolic-ref', 'HEAD'],
878 stderr=subprocess2.VOID, error_ok=True).strip()
879 if not branchref:
880 return None
881 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000882 self.branch = ShortBranchName(self.branchref)
883 return self.branch
884
885 def GetBranchRef(self):
886 """Returns the full branch name, e.g. 'refs/heads/master'."""
887 self.GetBranch() # Poke the lazy loader.
888 return self.branchref
889
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000890 @staticmethod
891 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000892 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000893 e.g. 'origin', 'refs/heads/master'
894 """
895 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000896 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
897 error_ok=True).strip()
898 if upstream_branch:
899 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
900 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000901 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
902 error_ok=True).strip()
903 if upstream_branch:
904 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000905 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000906 # Fall back on trying a git-svn upstream branch.
907 if settings.GetIsGitSvn():
908 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000909 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000910 # Else, try to guess the origin remote.
911 remote_branches = RunGit(['branch', '-r']).split()
912 if 'origin/master' in remote_branches:
913 # Fall back on origin/master if it exits.
914 remote = 'origin'
915 upstream_branch = 'refs/heads/master'
916 elif 'origin/trunk' in remote_branches:
917 # Fall back on origin/trunk if it exists. Generally a shared
918 # git-svn clone
919 remote = 'origin'
920 upstream_branch = 'refs/heads/trunk'
921 else:
922 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000923Either pass complete "git diff"-style arguments, like
924 git cl upload origin/master
925or verify this branch is set up to track another (via the --track argument to
926"git checkout -b ...").""")
927
928 return remote, upstream_branch
929
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000930 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000931 upstream_branch = self.GetUpstreamBranch()
932 if not BranchExists(upstream_branch):
933 DieWithError('The upstream for the current branch (%s) does not exist '
934 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +0000935 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000936 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000937
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000938 def GetUpstreamBranch(self):
939 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000940 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000941 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000942 upstream_branch = upstream_branch.replace('refs/heads/',
943 'refs/remotes/%s/' % remote)
944 upstream_branch = upstream_branch.replace('refs/branch-heads/',
945 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000946 self.upstream_branch = upstream_branch
947 return self.upstream_branch
948
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000949 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000950 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000951 remote, branch = None, self.GetBranch()
952 seen_branches = set()
953 while branch not in seen_branches:
954 seen_branches.add(branch)
955 remote, branch = self.FetchUpstreamTuple(branch)
956 branch = ShortBranchName(branch)
957 if remote != '.' or branch.startswith('refs/remotes'):
958 break
959 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000960 remotes = RunGit(['remote'], error_ok=True).split()
961 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000962 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000963 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000964 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000965 logging.warning('Could not determine which remote this change is '
966 'associated with, so defaulting to "%s". This may '
967 'not be what you want. You may prevent this message '
968 'by running "git svn info" as documented here: %s',
969 self._remote,
970 GIT_INSTRUCTIONS_URL)
971 else:
972 logging.warn('Could not determine which remote this change is '
973 'associated with. You may prevent this message by '
974 'running "git svn info" as documented here: %s',
975 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000976 branch = 'HEAD'
977 if branch.startswith('refs/remotes'):
978 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000979 elif branch.startswith('refs/branch-heads/'):
980 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000981 else:
982 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000983 return self._remote
984
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000985 def GitSanityChecks(self, upstream_git_obj):
986 """Checks git repo status and ensures diff is from local commits."""
987
sbc@chromium.org79706062015-01-14 21:18:12 +0000988 if upstream_git_obj is None:
989 if self.GetBranch() is None:
990 print >> sys.stderr, (
dpranke@chromium.orgee87f582015-07-31 18:46:25 +0000991 'ERROR: unable to determine current branch (detached HEAD?)')
sbc@chromium.org79706062015-01-14 21:18:12 +0000992 else:
993 print >> sys.stderr, (
994 'ERROR: no upstream branch')
995 return False
996
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000997 # Verify the commit we're diffing against is in our current branch.
998 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
999 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1000 if upstream_sha != common_ancestor:
1001 print >> sys.stderr, (
1002 'ERROR: %s is not in the current branch. You may need to rebase '
1003 'your tracking branch' % upstream_sha)
1004 return False
1005
1006 # List the commits inside the diff, and verify they are all local.
1007 commits_in_diff = RunGit(
1008 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1009 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1010 remote_branch = remote_branch.strip()
1011 if code != 0:
1012 _, remote_branch = self.GetRemoteBranch()
1013
1014 commits_in_remote = RunGit(
1015 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1016
1017 common_commits = set(commits_in_diff) & set(commits_in_remote)
1018 if common_commits:
1019 print >> sys.stderr, (
1020 'ERROR: Your diff contains %d commits already in %s.\n'
1021 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1022 'the diff. If you are using a custom git flow, you can override'
1023 ' the reference used for this check with "git config '
1024 'gitcl.remotebranch <git-ref>".' % (
1025 len(common_commits), remote_branch, upstream_git_obj))
1026 return False
1027 return True
1028
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001029 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001030 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001031
1032 Returns None if it is not set.
1033 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001034 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
1035 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001036
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001037 def GetGitSvnRemoteUrl(self):
1038 """Return the configured git-svn remote URL parsed from git svn info.
1039
1040 Returns None if it is not set.
1041 """
1042 # URL is dependent on the current directory.
1043 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1044 if data:
1045 keys = dict(line.split(': ', 1) for line in data.splitlines()
1046 if ': ' in line)
1047 return keys.get('URL', None)
1048 return None
1049
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001050 def GetRemoteUrl(self):
1051 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1052
1053 Returns None if there is no remote.
1054 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001055 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001056 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1057
1058 # If URL is pointing to a local directory, it is probably a git cache.
1059 if os.path.isdir(url):
1060 url = RunGit(['config', 'remote.%s.url' % remote],
1061 error_ok=True,
1062 cwd=url).strip()
1063 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001064
1065 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001066 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001067 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001068 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001069 self.issue = int(issue) or None if issue else None
1070 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001071 return self.issue
1072
1073 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +00001074 if not self.rietveld_server:
1075 # If we're on a branch then get the server potentially associated
1076 # with that branch.
1077 if self.GetIssue():
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001078 rietveld_server_config = self._RietveldServer()
1079 if rietveld_server_config:
1080 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1081 ['config', rietveld_server_config], error_ok=True).strip())
evan@chromium.org0af9b702012-02-11 00:42:16 +00001082 if not self.rietveld_server:
1083 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001084 return self.rietveld_server
1085
1086 def GetIssueURL(self):
1087 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001088 if not self.GetIssue():
1089 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001090 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
1091
1092 def GetDescription(self, pretty=False):
1093 if not self.has_description:
1094 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +00001095 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +00001096 try:
1097 self.description = self.RpcServer().get_description(issue).strip()
maruel@chromium.org85616e02014-07-28 15:37:55 +00001098 except urllib2.HTTPError as e:
miket@chromium.org183df1a2012-01-04 19:44:55 +00001099 if e.code == 404:
1100 DieWithError(
1101 ('\nWhile fetching the description for issue %d, received a '
1102 '404 (not found)\n'
1103 'error. It is likely that you deleted this '
1104 'issue on the server. If this is the\n'
1105 'case, please run\n\n'
1106 ' git cl issue 0\n\n'
1107 'to clear the association with the deleted issue. Then run '
1108 'this command again.') % issue)
1109 else:
1110 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +00001111 '\nFailed to fetch issue description. HTTP error %d' % e.code)
maruel@chromium.org85616e02014-07-28 15:37:55 +00001112 except urllib2.URLError as e:
1113 print >> sys.stderr, (
1114 'Warning: Failed to retrieve CL description due to network '
1115 'failure.')
1116 self.description = ''
1117
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001118 self.has_description = True
1119 if pretty:
1120 wrapper = textwrap.TextWrapper()
1121 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1122 return wrapper.fill(self.description)
1123 return self.description
1124
1125 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001126 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001127 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001128 patchset = RunGit(['config', self._PatchsetSetting()],
1129 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001130 self.patchset = int(patchset) or None if patchset else None
1131 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001132 return self.patchset
1133
1134 def SetPatchset(self, patchset):
1135 """Set this branch's patchset. If patchset=0, clears the patchset."""
1136 if patchset:
1137 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001138 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001139 else:
1140 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001141 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001142 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001143
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001144 def GetMostRecentPatchset(self):
1145 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +00001146
1147 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001148 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001149 '/download/issue%s_%s.diff' % (issue, patchset))
1150
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001151 def GetIssueProperties(self):
1152 if self._props is None:
1153 issue = self.GetIssue()
1154 if not issue:
1155 self._props = {}
1156 else:
1157 self._props = self.RpcServer().get_issue_properties(issue, True)
1158 return self._props
1159
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001160 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001161 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001162
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001163 def AddComment(self, message):
1164 return self.RpcServer().add_comment(self.GetIssue(), message)
1165
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001166 def SetIssue(self, issue):
1167 """Set this branch's issue. If issue=0, clears the issue."""
1168 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001169 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001170 RunGit(['config', self._IssueSetting(), str(issue)])
1171 if self.rietveld_server:
1172 RunGit(['config', self._RietveldServer(), self.rietveld_server])
1173 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +00001174 current_issue = self.GetIssue()
1175 if current_issue:
1176 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001177 self.issue = None
1178 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001179
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001180 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001181 if not self.GitSanityChecks(upstream_branch):
1182 DieWithError('\nGit sanity check failure')
1183
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001184 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001185 if not root:
1186 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001187 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001188
1189 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001190 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001191 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001192 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001193 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001194 except subprocess2.CalledProcessError:
1195 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001196 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001197 'This branch probably doesn\'t exist anymore. To reset the\n'
1198 'tracking branch, please run\n'
1199 ' git branch --set-upstream %s trunk\n'
1200 'replacing trunk with origin/master or the relevant branch') %
1201 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001202
maruel@chromium.org52424302012-08-29 15:14:30 +00001203 issue = self.GetIssue()
1204 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001205 if issue:
1206 description = self.GetDescription()
1207 else:
1208 # If the change was never uploaded, use the log messages of all commits
1209 # up to the branch point, as git cl upload will prefill the description
1210 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001211 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1212 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001213
1214 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001215 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001216 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001217 name,
1218 description,
1219 absroot,
1220 files,
1221 issue,
1222 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001223 author,
1224 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001225
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001226 def GetStatus(self):
1227 """Apply a rough heuristic to give a simple summary of an issue's review
1228 or CQ status, assuming adherence to a common workflow.
1229
1230 Returns None if no issue for this branch, or one of the following keywords:
1231 * 'error' - error from review tool (including deleted issues)
1232 * 'unsent' - not sent for review
1233 * 'waiting' - waiting for review
1234 * 'reply' - waiting for owner to reply to review
1235 * 'lgtm' - LGTM from at least one approved reviewer
1236 * 'commit' - in the commit queue
1237 * 'closed' - closed
1238 """
1239 if not self.GetIssue():
1240 return None
1241
1242 try:
1243 props = self.GetIssueProperties()
1244 except urllib2.HTTPError:
1245 return 'error'
1246
1247 if props.get('closed'):
1248 # Issue is closed.
1249 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001250 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001251 # Issue is in the commit queue.
1252 return 'commit'
1253
1254 try:
1255 reviewers = self.GetApprovingReviewers()
1256 except urllib2.HTTPError:
1257 return 'error'
1258
1259 if reviewers:
1260 # Was LGTM'ed.
1261 return 'lgtm'
1262
1263 messages = props.get('messages') or []
1264
1265 if not messages:
1266 # No message was sent.
1267 return 'unsent'
1268 if messages[-1]['sender'] != props.get('owner_email'):
1269 # Non-LGTM reply from non-owner
1270 return 'reply'
1271 return 'waiting'
1272
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001273 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001274 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001275
1276 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001277 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001278 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001279 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001280 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001281 except presubmit_support.PresubmitFailure, e:
1282 DieWithError(
1283 ('%s\nMaybe your depot_tools is out of date?\n'
1284 'If all fails, contact maruel@') % e)
1285
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001286 def UpdateDescription(self, description):
1287 self.description = description
1288 return self.RpcServer().update_description(
1289 self.GetIssue(), self.description)
1290
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001291 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +00001292 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001293 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001294
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001295 def SetFlag(self, flag, value):
1296 """Patchset must match."""
1297 if not self.GetPatchset():
1298 DieWithError('The patchset needs to match. Send another patchset.')
1299 try:
1300 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001301 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001302 except urllib2.HTTPError, e:
1303 if e.code == 404:
1304 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1305 if e.code == 403:
1306 DieWithError(
1307 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1308 'match?') % (self.GetIssue(), self.GetPatchset()))
1309 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001310
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001311 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001312 """Returns an upload.RpcServer() to access this review's rietveld instance.
1313 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001314 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001315 self._rpc_server = rietveld.CachingRietveld(
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001316 self.GetRietveldServer(),
1317 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001318 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001319
1320 def _IssueSetting(self):
1321 """Return the git setting that stores this change's issue."""
1322 return 'branch.%s.rietveldissue' % self.GetBranch()
1323
1324 def _PatchsetSetting(self):
1325 """Return the git setting that stores this change's most recent patchset."""
1326 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1327
1328 def _RietveldServer(self):
1329 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001330 branch = self.GetBranch()
1331 if branch:
1332 return 'branch.%s.rietveldserver' % branch
1333 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001334
1335
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001336class ChangeDescription(object):
1337 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001338 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00001339 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001340
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001341 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00001342 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001343
agable@chromium.org42c20792013-09-12 17:34:49 +00001344 @property # www.logilab.org/ticket/89786
1345 def description(self): # pylint: disable=E0202
1346 return '\n'.join(self._description_lines)
1347
1348 def set_description(self, desc):
1349 if isinstance(desc, basestring):
1350 lines = desc.splitlines()
1351 else:
1352 lines = [line.rstrip() for line in desc]
1353 while lines and not lines[0]:
1354 lines.pop(0)
1355 while lines and not lines[-1]:
1356 lines.pop(-1)
1357 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001358
piman@chromium.org336f9122014-09-04 02:16:55 +00001359 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001360 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001361 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001362 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001363 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001364 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001365
agable@chromium.org42c20792013-09-12 17:34:49 +00001366 # Get the set of R= and TBR= lines and remove them from the desciption.
1367 regexp = re.compile(self.R_LINE)
1368 matches = [regexp.match(line) for line in self._description_lines]
1369 new_desc = [l for i, l in enumerate(self._description_lines)
1370 if not matches[i]]
1371 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001372
agable@chromium.org42c20792013-09-12 17:34:49 +00001373 # Construct new unified R= and TBR= lines.
1374 r_names = []
1375 tbr_names = []
1376 for match in matches:
1377 if not match:
1378 continue
1379 people = cleanup_list([match.group(2).strip()])
1380 if match.group(1) == 'TBR':
1381 tbr_names.extend(people)
1382 else:
1383 r_names.extend(people)
1384 for name in r_names:
1385 if name not in reviewers:
1386 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001387 if add_owners_tbr:
1388 owners_db = owners.Database(change.RepositoryRoot(),
1389 fopen=file, os_path=os.path, glob=glob.glob)
1390 all_reviewers = set(tbr_names + reviewers)
1391 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1392 all_reviewers)
1393 tbr_names.extend(owners_db.reviewers_for(missing_files,
1394 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001395 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1396 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1397
1398 # Put the new lines in the description where the old first R= line was.
1399 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1400 if 0 <= line_loc < len(self._description_lines):
1401 if new_tbr_line:
1402 self._description_lines.insert(line_loc, new_tbr_line)
1403 if new_r_line:
1404 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001405 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001406 if new_r_line:
1407 self.append_footer(new_r_line)
1408 if new_tbr_line:
1409 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001410
1411 def prompt(self):
1412 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001413 self.set_description([
1414 '# Enter a description of the change.',
1415 '# This will be displayed on the codereview site.',
1416 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00001417 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00001418 '--------------------',
1419 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001420
agable@chromium.org42c20792013-09-12 17:34:49 +00001421 regexp = re.compile(self.BUG_LINE)
1422 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00001423 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00001424 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001425 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001426 if not content:
1427 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001428 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001429
1430 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001431 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1432 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001433 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001434 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001435
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001436 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001437 if self._description_lines:
1438 # Add an empty line if either the last line or the new line isn't a tag.
1439 last_line = self._description_lines[-1]
1440 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1441 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1442 self._description_lines.append('')
1443 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001444
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001445 def get_reviewers(self):
1446 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001447 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1448 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001449 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001450
1451
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001452def get_approving_reviewers(props):
1453 """Retrieves the reviewers that approved a CL from the issue properties with
1454 messages.
1455
1456 Note that the list may contain reviewers that are not committer, thus are not
1457 considered by the CQ.
1458 """
1459 return sorted(
1460 set(
1461 message['sender']
1462 for message in props['messages']
1463 if message['approval'] and message['sender'] in props['reviewers']
1464 )
1465 )
1466
1467
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001468def FindCodereviewSettingsFile(filename='codereview.settings'):
1469 """Finds the given file starting in the cwd and going up.
1470
1471 Only looks up to the top of the repository unless an
1472 'inherit-review-settings-ok' file exists in the root of the repository.
1473 """
1474 inherit_ok_file = 'inherit-review-settings-ok'
1475 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001476 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001477 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1478 root = '/'
1479 while True:
1480 if filename in os.listdir(cwd):
1481 if os.path.isfile(os.path.join(cwd, filename)):
1482 return open(os.path.join(cwd, filename))
1483 if cwd == root:
1484 break
1485 cwd = os.path.dirname(cwd)
1486
1487
1488def LoadCodereviewSettingsFromFile(fileobj):
1489 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001490 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001491
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001492 def SetProperty(name, setting, unset_error_ok=False):
1493 fullname = 'rietveld.' + name
1494 if setting in keyvals:
1495 RunGit(['config', fullname, keyvals[setting]])
1496 else:
1497 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1498
1499 SetProperty('server', 'CODE_REVIEW_SERVER')
1500 # Only server setting is required. Other settings can be absent.
1501 # In that case, we ignore errors raised during option deletion attempt.
1502 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001503 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001504 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1505 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001506 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001507 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001508 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
1509 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001510 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001511 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001512 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00001513 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
1514 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001515
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001516 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001517 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001518
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00001519 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
1520 RunGit(['config', 'gerrit.squash-uploads',
1521 keyvals['GERRIT_SQUASH_UPLOADS']])
1522
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001523 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1524 #should be of the form
1525 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1526 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1527 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1528 keyvals['ORIGIN_URL_CONFIG']])
1529
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001530
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001531def urlretrieve(source, destination):
1532 """urllib is broken for SSL connections via a proxy therefore we
1533 can't use urllib.urlretrieve()."""
1534 with open(destination, 'w') as f:
1535 f.write(urllib2.urlopen(source).read())
1536
1537
ukai@chromium.org712d6102013-11-27 00:52:58 +00001538def hasSheBang(fname):
1539 """Checks fname is a #! script."""
1540 with open(fname) as f:
1541 return f.read(2).startswith('#!')
1542
1543
tandrii@chromium.org18630d62016-03-04 12:06:02 +00001544def DownloadGerritHook(force):
1545 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001546
1547 Args:
1548 force: True to update hooks. False to install hooks if not present.
1549 """
1550 if not settings.GetIsGerrit():
1551 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001552 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001553 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1554 if not os.access(dst, os.X_OK):
1555 if os.path.exists(dst):
1556 if not force:
1557 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001558 try:
tandrii@chromium.org18630d62016-03-04 12:06:02 +00001559 print(
1560 'WARNING: installing Gerrit commit-msg hook.\n'
1561 ' This behavior of git cl will soon be disabled.\n'
1562 ' See bug http://crbug.com/579176.')
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001563 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001564 if not hasSheBang(dst):
1565 DieWithError('Not a script: %s\n'
1566 'You need to download from\n%s\n'
1567 'into .git/hooks/commit-msg and '
1568 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001569 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1570 except Exception:
1571 if os.path.exists(dst):
1572 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001573 DieWithError('\nFailed to download hooks.\n'
1574 'You need to download from\n%s\n'
1575 'into .git/hooks/commit-msg and '
1576 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001577
tandrii@chromium.orgc55295c2016-03-04 15:54:59 +00001578# TODO(tandrii): remove this once repos which call this method directly are
tandrii@chromium.org675bec32016-03-04 16:36:58 +00001579# upgraded. See http://crbug.com/579176.
tandrii@chromium.orgc55295c2016-03-04 15:54:59 +00001580DownloadHooks = DownloadGerritHook
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001581
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00001582
1583def GetRietveldCodereviewSettingsInteractively():
1584 """Prompt the user for settings."""
1585 server = settings.GetDefaultServerUrl(error_ok=True)
1586 prompt = 'Rietveld server (host[:port])'
1587 prompt += ' [%s]' % (server or DEFAULT_SERVER)
1588 newserver = ask_for_data(prompt + ':')
1589 if not server and not newserver:
1590 newserver = DEFAULT_SERVER
1591 if newserver:
1592 newserver = gclient_utils.UpgradeToHttps(newserver)
1593 if newserver != server:
1594 RunGit(['config', 'rietveld.server', newserver])
1595
1596 def SetProperty(initial, caption, name, is_url):
1597 prompt = caption
1598 if initial:
1599 prompt += ' ("x" to clear) [%s]' % initial
1600 new_val = ask_for_data(prompt + ':')
1601 if new_val == 'x':
1602 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
1603 elif new_val:
1604 if is_url:
1605 new_val = gclient_utils.UpgradeToHttps(new_val)
1606 if new_val != initial:
1607 RunGit(['config', 'rietveld.' + name, new_val])
1608
1609 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
1610 SetProperty(settings.GetDefaultPrivateFlag(),
1611 'Private flag (rietveld only)', 'private', False)
1612 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
1613 'tree-status-url', False)
1614 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
1615 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
1616 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
1617 'run-post-upload-hook', False)
1618
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001619@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001620def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001621 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001622
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00001623 print('WARNING: git cl config works for Rietveld only.\n'
1624 'For Gerrit, see http://crbug.com/579160.')
1625 # TODO(tandrii): add Gerrit support as part of http://crbug.com/579160.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001626 parser.add_option('--activate-update', action='store_true',
1627 help='activate auto-updating [rietveld] section in '
1628 '.git/config')
1629 parser.add_option('--deactivate-update', action='store_true',
1630 help='deactivate auto-updating [rietveld] section in '
1631 '.git/config')
1632 options, args = parser.parse_args(args)
1633
1634 if options.deactivate_update:
1635 RunGit(['config', 'rietveld.autoupdate', 'false'])
1636 return
1637
1638 if options.activate_update:
1639 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1640 return
1641
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001642 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00001643 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001644 return 0
1645
1646 url = args[0]
1647 if not url.endswith('codereview.settings'):
1648 url = os.path.join(url, 'codereview.settings')
1649
1650 # Load code review settings and download hooks (if available).
1651 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
1652 return 0
1653
1654
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001655def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001656 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001657 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1658 branch = ShortBranchName(branchref)
1659 _, args = parser.parse_args(args)
1660 if not args:
1661 print("Current base-url:")
1662 return RunGit(['config', 'branch.%s.base-url' % branch],
1663 error_ok=False).strip()
1664 else:
1665 print("Setting base-url to %s" % args[0])
1666 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1667 error_ok=False).strip()
1668
1669
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001670def color_for_status(status):
1671 """Maps a Changelist status to color, for CMDstatus and other tools."""
1672 return {
1673 'unsent': Fore.RED,
1674 'waiting': Fore.BLUE,
1675 'reply': Fore.YELLOW,
1676 'lgtm': Fore.GREEN,
1677 'commit': Fore.MAGENTA,
1678 'closed': Fore.CYAN,
1679 'error': Fore.WHITE,
1680 }.get(status, Fore.WHITE)
1681
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001682def fetch_cl_status(branch, auth_config=None):
1683 """Fetches information for an issue and returns (branch, issue, status)."""
1684 cl = Changelist(branchref=branch, auth_config=auth_config)
1685 url = cl.GetIssueURL()
1686 status = cl.GetStatus()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001687
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001688 if url and (not status or status == 'error'):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001689 # The issue probably doesn't exist anymore.
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001690 url += ' (broken)'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001691
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001692 return (branch, url, status)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001693
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001694def get_cl_statuses(
1695 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001696 """Returns a blocking iterable of (branch, issue, color) for given branches.
1697
1698 If fine_grained is true, this will fetch CL statuses from the server.
1699 Otherwise, simply indicate if there's a matching url for the given branches.
1700
1701 If max_processes is specified, it is used as the maximum number of processes
1702 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
1703 spawned.
1704 """
1705 # Silence upload.py otherwise it becomes unwieldly.
1706 upload.verbosity = 0
1707
1708 if fine_grained:
1709 # Process one branch synchronously to work through authentication, then
1710 # spawn processes to process all the other branches in parallel.
1711 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001712 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
1713 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001714
1715 branches_to_fetch = branches[1:]
1716 pool = ThreadPool(
1717 min(max_processes, len(branches_to_fetch))
1718 if max_processes is not None
1719 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001720 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001721 yield x
1722 else:
1723 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1724 for b in branches:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001725 cl = Changelist(branchref=b, auth_config=auth_config)
1726 url = cl.GetIssueURL()
1727 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001728
rmistry@google.com2dd99862015-06-22 12:22:18 +00001729
1730def upload_branch_deps(cl, args):
1731 """Uploads CLs of local branches that are dependents of the current branch.
1732
1733 If the local branch dependency tree looks like:
1734 test1 -> test2.1 -> test3.1
1735 -> test3.2
1736 -> test2.2 -> test3.3
1737
1738 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
1739 run on the dependent branches in this order:
1740 test2.1, test3.1, test3.2, test2.2, test3.3
1741
1742 Note: This function does not rebase your local dependent branches. Use it when
1743 you make a change to the parent branch that will not conflict with its
1744 dependent branches, and you would like their dependencies updated in
1745 Rietveld.
1746 """
1747 if git_common.is_dirty_git_tree('upload-branch-deps'):
1748 return 1
1749
1750 root_branch = cl.GetBranch()
1751 if root_branch is None:
1752 DieWithError('Can\'t find dependent branches from detached HEAD state. '
1753 'Get on a branch!')
1754 if not cl.GetIssue() or not cl.GetPatchset():
1755 DieWithError('Current branch does not have an uploaded CL. We cannot set '
1756 'patchset dependencies without an uploaded CL.')
1757
1758 branches = RunGit(['for-each-ref',
1759 '--format=%(refname:short) %(upstream:short)',
1760 'refs/heads'])
1761 if not branches:
1762 print('No local branches found.')
1763 return 0
1764
1765 # Create a dictionary of all local branches to the branches that are dependent
1766 # on it.
1767 tracked_to_dependents = collections.defaultdict(list)
1768 for b in branches.splitlines():
1769 tokens = b.split()
1770 if len(tokens) == 2:
1771 branch_name, tracked = tokens
1772 tracked_to_dependents[tracked].append(branch_name)
1773
1774 print
1775 print 'The dependent local branches of %s are:' % root_branch
1776 dependents = []
1777 def traverse_dependents_preorder(branch, padding=''):
1778 dependents_to_process = tracked_to_dependents.get(branch, [])
1779 padding += ' '
1780 for dependent in dependents_to_process:
1781 print '%s%s' % (padding, dependent)
1782 dependents.append(dependent)
1783 traverse_dependents_preorder(dependent, padding)
1784 traverse_dependents_preorder(root_branch)
1785 print
1786
1787 if not dependents:
1788 print 'There are no dependent local branches for %s' % root_branch
1789 return 0
1790
1791 print ('This command will checkout all dependent branches and run '
1792 '"git cl upload".')
1793 ask_for_data('[Press enter to continue or ctrl-C to quit]')
1794
andybons@chromium.org962f9462016-02-03 20:00:42 +00001795 # Add a default patchset title to all upload calls in Rietveld.
1796 if not settings.GetIsGerrit():
1797 args.extend(['-t', 'Updated patchset dependency'])
1798
rmistry@google.com2dd99862015-06-22 12:22:18 +00001799 # Record all dependents that failed to upload.
1800 failures = {}
1801 # Go through all dependents, checkout the branch and upload.
1802 try:
1803 for dependent_branch in dependents:
1804 print
1805 print '--------------------------------------'
1806 print 'Running "git cl upload" from %s:' % dependent_branch
1807 RunGit(['checkout', '-q', dependent_branch])
1808 print
1809 try:
1810 if CMDupload(OptionParser(), args) != 0:
1811 print 'Upload failed for %s!' % dependent_branch
1812 failures[dependent_branch] = 1
1813 except: # pylint: disable=W0702
1814 failures[dependent_branch] = 1
1815 print
1816 finally:
1817 # Swap back to the original root branch.
1818 RunGit(['checkout', '-q', root_branch])
1819
1820 print
1821 print 'Upload complete for dependent branches!'
1822 for dependent_branch in dependents:
1823 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
1824 print ' %s : %s' % (dependent_branch, upload_status)
1825 print
1826
1827 return 0
1828
1829
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001830def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001831 """Show status of changelists.
1832
1833 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001834 - Red not sent for review or broken
1835 - Blue waiting for review
1836 - Yellow waiting for you to reply to review
1837 - Green LGTM'ed
1838 - Magenta in the commit queue
1839 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001840
1841 Also see 'git cl comments'.
1842 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001843 parser.add_option('--field',
1844 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001845 parser.add_option('-f', '--fast', action='store_true',
1846 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001847 parser.add_option(
1848 '-j', '--maxjobs', action='store', type=int,
1849 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001850
1851 auth.add_auth_options(parser)
1852 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001853 if args:
1854 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001855 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001856
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001857 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001858 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001859 if options.field.startswith('desc'):
1860 print cl.GetDescription()
1861 elif options.field == 'id':
1862 issueid = cl.GetIssue()
1863 if issueid:
1864 print issueid
1865 elif options.field == 'patch':
1866 patchset = cl.GetPatchset()
1867 if patchset:
1868 print patchset
1869 elif options.field == 'url':
1870 url = cl.GetIssueURL()
1871 if url:
1872 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001873 return 0
1874
1875 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1876 if not branches:
1877 print('No local branch found.')
1878 return 0
1879
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001880 changes = (
1881 Changelist(branchref=b, auth_config=auth_config)
1882 for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001883 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001884 alignment = max(5, max(len(b) for b in branches))
1885 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001886 output = get_cl_statuses(branches,
1887 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001888 max_processes=options.maxjobs,
1889 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001890
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001891 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001892 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001893 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001894 while branch not in branch_statuses:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001895 b, i, status = output.next()
1896 branch_statuses[b] = (i, status)
1897 issue_url, status = branch_statuses.pop(branch)
1898 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001899 reset = Fore.RESET
1900 if not sys.stdout.isatty():
1901 color = ''
1902 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001903 status_str = '(%s)' % status if status else ''
1904 print ' %*s : %s%s %s%s' % (
1905 alignment, ShortBranchName(branch), color, issue_url, status_str,
1906 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001907
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001908 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001909 print
1910 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001911 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00001912 if not cl.GetIssue():
1913 print 'No issue assigned.'
1914 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001915 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00001916 if not options.fast:
1917 print 'Issue description:'
1918 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001919 return 0
1920
1921
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001922def colorize_CMDstatus_doc():
1923 """To be called once in main() to add colors to git cl status help."""
1924 colors = [i for i in dir(Fore) if i[0].isupper()]
1925
1926 def colorize_line(line):
1927 for color in colors:
1928 if color in line.upper():
1929 # Extract whitespaces first and the leading '-'.
1930 indent = len(line) - len(line.lstrip(' ')) + 1
1931 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1932 return line
1933
1934 lines = CMDstatus.__doc__.splitlines()
1935 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1936
1937
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001938@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001939def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001940 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001941
1942 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001943 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00001944 parser.add_option('-r', '--reverse', action='store_true',
1945 help='Lookup the branch(es) for the specified issues. If '
1946 'no issues are specified, all branches with mapped '
1947 'issues will be listed.')
1948 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001949
dnj@chromium.org406c4402015-03-03 17:22:28 +00001950 if options.reverse:
1951 branches = RunGit(['for-each-ref', 'refs/heads',
1952 '--format=%(refname:short)']).splitlines()
1953
1954 # Reverse issue lookup.
1955 issue_branch_map = {}
1956 for branch in branches:
1957 cl = Changelist(branchref=branch)
1958 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
1959 if not args:
1960 args = sorted(issue_branch_map.iterkeys())
1961 for issue in args:
1962 if not issue:
1963 continue
1964 print 'Branch for issue number %s: %s' % (
1965 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
1966 else:
1967 cl = Changelist()
1968 if len(args) > 0:
1969 try:
1970 issue = int(args[0])
1971 except ValueError:
1972 DieWithError('Pass a number to set the issue or none to list it.\n'
1973 'Maybe you want to run git cl status?')
1974 cl.SetIssue(issue)
1975 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001976 return 0
1977
1978
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001979def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001980 """Shows or posts review comments for any changelist."""
1981 parser.add_option('-a', '--add-comment', dest='comment',
1982 help='comment to add to an issue')
1983 parser.add_option('-i', dest='issue',
1984 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00001985 parser.add_option('-j', '--json-file',
1986 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001987 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001988 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001989 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001990
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001991 issue = None
1992 if options.issue:
1993 try:
1994 issue = int(options.issue)
1995 except ValueError:
1996 DieWithError('A review issue id is expected to be a number')
1997
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001998 cl = Changelist(issue=issue, auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001999
2000 if options.comment:
2001 cl.AddComment(options.comment)
2002 return 0
2003
2004 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00002005 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00002006 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00002007 summary.append({
2008 'date': message['date'],
2009 'lgtm': False,
2010 'message': message['text'],
2011 'not_lgtm': False,
2012 'sender': message['sender'],
2013 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002014 if message['disapproval']:
2015 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00002016 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002017 elif message['approval']:
2018 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00002019 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002020 elif message['sender'] == data['owner_email']:
2021 color = Fore.MAGENTA
2022 else:
2023 color = Fore.BLUE
2024 print '\n%s%s %s%s' % (
2025 color, message['date'].split('.', 1)[0], message['sender'],
2026 Fore.RESET)
2027 if message['text'].strip():
2028 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00002029 if options.json_file:
2030 with open(options.json_file, 'wb') as f:
2031 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002032 return 0
2033
2034
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002035def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002036 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00002037 parser.add_option('-d', '--display', action='store_true',
2038 help='Display the description instead of opening an editor')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002039 auth.add_auth_options(parser)
2040 options, _ = parser.parse_args(args)
2041 auth_config = auth.extract_auth_config_from_options(options)
2042 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002043 if not cl.GetIssue():
2044 DieWithError('This branch has no associated changelist.')
2045 description = ChangeDescription(cl.GetDescription())
smut@google.com34fb6b12015-07-13 20:03:26 +00002046 if options.display:
2047 print description.description
2048 return 0
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002049 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00002050 if cl.GetDescription() != description.description:
2051 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002052 return 0
2053
2054
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002055def CreateDescriptionFromLog(args):
2056 """Pulls out the commit log to use as a base for the CL description."""
2057 log_args = []
2058 if len(args) == 1 and not args[0].endswith('.'):
2059 log_args = [args[0] + '..']
2060 elif len(args) == 1 and args[0].endswith('...'):
2061 log_args = [args[0][:-1]]
2062 elif len(args) == 2:
2063 log_args = [args[0] + '..' + args[1]]
2064 else:
2065 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00002066 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002067
2068
thestig@chromium.org44202a22014-03-11 19:22:18 +00002069def CMDlint(parser, args):
2070 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002071 parser.add_option('--filter', action='append', metavar='-x,+y',
2072 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002073 auth.add_auth_options(parser)
2074 options, args = parser.parse_args(args)
2075 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002076
2077 # Access to a protected member _XX of a client class
2078 # pylint: disable=W0212
2079 try:
2080 import cpplint
2081 import cpplint_chromium
2082 except ImportError:
2083 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
2084 return 1
2085
2086 # Change the current working directory before calling lint so that it
2087 # shows the correct base.
2088 previous_cwd = os.getcwd()
2089 os.chdir(settings.GetRoot())
2090 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002091 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002092 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
2093 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00002094 if not files:
2095 print "Cannot lint an empty CL"
2096 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00002097
2098 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002099 command = args + files
2100 if options.filter:
2101 command = ['--filter=' + ','.join(options.filter)] + command
2102 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002103
2104 white_regex = re.compile(settings.GetLintRegex())
2105 black_regex = re.compile(settings.GetLintIgnoreRegex())
2106 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
2107 for filename in filenames:
2108 if white_regex.match(filename):
2109 if black_regex.match(filename):
2110 print "Ignoring file %s" % filename
2111 else:
2112 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
2113 extra_check_functions)
2114 else:
2115 print "Skipping file %s" % filename
2116 finally:
2117 os.chdir(previous_cwd)
2118 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
2119 if cpplint._cpplint_state.error_count != 0:
2120 return 1
2121 return 0
2122
2123
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002124def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002125 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002126 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002127 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002128 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00002129 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002130 auth.add_auth_options(parser)
2131 options, args = parser.parse_args(args)
2132 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002133
sbc@chromium.org71437c02015-04-09 19:29:40 +00002134 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00002135 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002136 return 1
2137
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002138 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002139 if args:
2140 base_branch = args[0]
2141 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002142 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002143 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002144
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002145 cl.RunHook(
2146 committing=not options.upload,
2147 may_prompt=False,
2148 verbose=options.verbose,
2149 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00002150 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002151
2152
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002153def AddChangeIdToCommitMessage(options, args):
2154 """Re-commits using the current message, assumes the commit hook is in
2155 place.
2156 """
2157 log_desc = options.message or CreateDescriptionFromLog(args)
2158 git_command = ['commit', '--amend', '-m', log_desc]
2159 RunGit(git_command)
2160 new_log_desc = CreateDescriptionFromLog(args)
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00002161 if git_footers.get_footer_change_id(new_log_desc):
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002162 print 'git-cl: Added Change-Id to commit message.'
2163 else:
2164 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
2165
2166
tandrii@chromium.org65874e12016-03-04 12:03:02 +00002167def GenerateGerritChangeId(message):
2168 """Returns Ixxxxxx...xxx change id.
2169
2170 Works the same way as
2171 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
2172 but can be called on demand on all platforms.
2173
2174 The basic idea is to generate git hash of a state of the tree, original commit
2175 message, author/committer info and timestamps.
2176 """
2177 lines = []
2178 tree_hash = RunGitSilent(['write-tree'])
2179 lines.append('tree %s' % tree_hash.strip())
2180 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
2181 if code == 0:
2182 lines.append('parent %s' % parent.strip())
2183 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
2184 lines.append('author %s' % author.strip())
2185 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
2186 lines.append('committer %s' % committer.strip())
2187 lines.append('')
2188 # Note: Gerrit's commit-hook actually cleans message of some lines and
2189 # whitespace. This code is not doing this, but it clearly won't decrease
2190 # entropy.
2191 lines.append(message)
2192 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
2193 stdin='\n'.join(lines))
2194 return 'I%s' % change_hash.strip()
2195
2196
piman@chromium.org336f9122014-09-04 02:16:55 +00002197def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002198 """upload the current branch to gerrit."""
2199 # We assume the remote called "origin" is the one we want.
2200 # It is probably not worthwhile to support different workflows.
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002201 gerrit_remote = 'origin'
luqui@chromium.org609f3952015-05-04 22:47:04 +00002202
2203 remote, remote_branch = cl.GetRemoteBranch()
2204 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2205 pending_prefix='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002206
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002207 change_desc = ChangeDescription(
2208 options.message or CreateDescriptionFromLog(args))
2209 if not change_desc.description:
andybons@chromium.org962f9462016-02-03 20:00:42 +00002210 print "\nDescription is empty. Aborting..."
2211 return 1
2212
2213 if options.title:
2214 print "\nPatch titles (-t) are not supported in Gerrit. Aborting..."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002215 return 1
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002216
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002217 if options.squash:
2218 # Try to get the message from a previous upload.
2219 shadow_branch = 'refs/heads/git_cl_uploads/' + cl.GetBranch()
bauerb@chromium.org13502e02016-02-18 10:18:29 +00002220 message = RunGitSilent(['show', '--format=%B', '-s', shadow_branch])
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002221 if not message:
2222 if not options.force:
2223 change_desc.prompt()
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002224 if not change_desc.description:
2225 print "Description is empty; aborting."
2226 return 1
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002227 message = change_desc.description
tandrii@chromium.org57d86542016-03-04 16:11:32 +00002228 change_ids = git_footers.get_footer_change_id(message)
2229 if len(change_ids) > 1:
2230 DieWithError('too many Change-Id footers in %s branch' % shadow_branch)
2231 if not change_ids:
2232 message = git_footers.add_footer_change_id(
2233 message, GenerateGerritChangeId(message))
2234 change_desc.set_description(message)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002235
2236 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2237 if remote is '.':
2238 # If our upstream branch is local, we base our squashed commit on its
2239 # squashed version.
2240 parent = ('refs/heads/git_cl_uploads/' +
2241 scm.GIT.ShortBranchName(upstream_branch))
2242
2243 # Verify that the upstream branch has been uploaded too, otherwise Gerrit
2244 # will create additional CLs when uploading.
2245 if (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2246 RunGitSilent(['rev-parse', parent + ':'])):
2247 print 'Upload upstream branch ' + upstream_branch + ' first.'
2248 return 1
2249 else:
2250 parent = cl.GetCommonAncestorWithUpstream()
2251
2252 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2253 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2254 '-m', message]).strip()
2255 else:
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00002256 if not git_footers.get_footer_change_id(change_desc.description):
tandrii@chromium.org10625002016-03-04 20:03:47 +00002257 DownloadGerritHook(False)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002258 AddChangeIdToCommitMessage(options, args)
2259 ref_to_push = 'HEAD'
2260 parent = '%s/%s' % (gerrit_remote, branch)
2261
2262 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2263 ref_to_push)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002264 if len(commits) > 1:
2265 print('WARNING: This will upload %d commits. Run the following command '
2266 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002267 print('git log %s..%s' % (parent, ref_to_push))
2268 print('You can also use `git squash-branch` to squash these into a single '
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002269 'commit.')
2270 ask_for_data('About to upload; enter to confirm.')
2271
piman@chromium.org336f9122014-09-04 02:16:55 +00002272 if options.reviewers or options.tbr_owners:
2273 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002274
ukai@chromium.orge8077812012-02-03 03:41:46 +00002275 receive_options = []
2276 cc = cl.GetCCList().split(',')
2277 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002278 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002279 cc = filter(None, cc)
2280 if cc:
2281 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002282 if change_desc.get_reviewers():
2283 receive_options.extend(
2284 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002285
ukai@chromium.orge8077812012-02-03 03:41:46 +00002286 git_command = ['push']
2287 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00002288 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00002289 ' '.join(receive_options))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002290 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
ukai@chromium.orge8077812012-02-03 03:41:46 +00002291 RunGit(git_command)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002292
2293 if options.squash:
2294 head = RunGit(['rev-parse', 'HEAD']).strip()
2295 RunGit(['update-ref', '-m', 'Uploaded ' + head, shadow_branch, ref_to_push])
2296
ukai@chromium.orge8077812012-02-03 03:41:46 +00002297 # TODO(ukai): parse Change-Id: and set issue number?
2298 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002299
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002300
wittman@chromium.org455dc922015-01-26 20:15:50 +00002301def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
2302 """Computes the remote branch ref to use for the CL.
2303
2304 Args:
2305 remote (str): The git remote for the CL.
2306 remote_branch (str): The git remote branch for the CL.
2307 target_branch (str): The target branch specified by the user.
2308 pending_prefix (str): The pending prefix from the settings.
2309 """
2310 if not (remote and remote_branch):
2311 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002312
wittman@chromium.org455dc922015-01-26 20:15:50 +00002313 if target_branch:
2314 # Cannonicalize branch references to the equivalent local full symbolic
2315 # refs, which are then translated into the remote full symbolic refs
2316 # below.
2317 if '/' not in target_branch:
2318 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
2319 else:
2320 prefix_replacements = (
2321 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
2322 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
2323 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
2324 )
2325 match = None
2326 for regex, replacement in prefix_replacements:
2327 match = re.search(regex, target_branch)
2328 if match:
2329 remote_branch = target_branch.replace(match.group(0), replacement)
2330 break
2331 if not match:
2332 # This is a branch path but not one we recognize; use as-is.
2333 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00002334 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
2335 # Handle the refs that need to land in different refs.
2336 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002337
wittman@chromium.org455dc922015-01-26 20:15:50 +00002338 # Create the true path to the remote branch.
2339 # Does the following translation:
2340 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
2341 # * refs/remotes/origin/master -> refs/heads/master
2342 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
2343 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
2344 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
2345 elif remote_branch.startswith('refs/remotes/%s/' % remote):
2346 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
2347 'refs/heads/')
2348 elif remote_branch.startswith('refs/remotes/branch-heads'):
2349 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
2350 # If a pending prefix exists then replace refs/ with it.
2351 if pending_prefix:
2352 remote_branch = remote_branch.replace('refs/', pending_prefix)
2353 return remote_branch
2354
2355
piman@chromium.org336f9122014-09-04 02:16:55 +00002356def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002357 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002358 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2359 upload_args.extend(['--server', cl.GetRietveldServer()])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002360 upload_args.extend(auth.auth_config_to_command_options(cl.auth_config))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002361 if options.emulate_svn_auto_props:
2362 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002363
2364 change_desc = None
2365
pgervais@chromium.org91141372014-01-09 23:27:20 +00002366 if options.email is not None:
2367 upload_args.extend(['--email', options.email])
2368
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002369 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002370 if options.title:
2371 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00002372 if options.message:
2373 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00002374 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002375 print ("This branch is associated with issue %s. "
2376 "Adding patch to that issue." % cl.GetIssue())
2377 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002378 if options.title:
2379 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00002380 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002381 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00002382 if options.reviewers or options.tbr_owners:
2383 change_desc.update_reviewers(options.reviewers,
2384 options.tbr_owners,
2385 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002386 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002387 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002388
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002389 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002390 print "Description is empty; aborting."
2391 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002392
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002393 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002394 if change_desc.get_reviewers():
2395 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00002396 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002397 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00002398 DieWithError("Must specify reviewers to send email.")
2399 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00002400
2401 # We check this before applying rietveld.private assuming that in
2402 # rietveld.cc only addresses which we can send private CLs to are listed
2403 # if rietveld.private is set, and so we should ignore rietveld.cc only when
2404 # --private is specified explicitly on the command line.
2405 if options.private:
2406 logging.warn('rietveld.cc is ignored since private flag is specified. '
2407 'You need to review and add them manually if necessary.')
2408 cc = cl.GetCCListWithoutDefault()
2409 else:
2410 cc = cl.GetCCList()
2411 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00002412 if cc:
2413 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002414
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002415 if options.private or settings.GetDefaultPrivateFlag() == "True":
2416 upload_args.append('--private')
2417
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002418 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00002419 if not options.find_copies:
2420 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002421
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002422 # Include the upstream repo's URL in the change -- this is useful for
2423 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002424 remote_url = cl.GetGitBaseUrlFromConfig()
2425 if not remote_url:
2426 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002427 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002428 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00002429 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
2430 remote_url = (cl.GetRemoteUrl() + '@'
2431 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002432 if remote_url:
2433 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00002434 remote, remote_branch = cl.GetRemoteBranch()
wittman@chromium.org455dc922015-01-26 20:15:50 +00002435 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2436 settings.GetPendingRefPrefix())
2437 if target_ref:
2438 upload_args.extend(['--target_ref', target_ref])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002439
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002440 # Look for dependent patchsets. See crbug.com/480453 for more details.
2441 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2442 upstream_branch = ShortBranchName(upstream_branch)
2443 if remote is '.':
2444 # A local branch is being tracked.
2445 local_branch = ShortBranchName(upstream_branch)
rmistry@google.com78948ed2015-07-08 23:09:57 +00002446 if settings.GetIsSkipDependencyUpload(local_branch):
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002447 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002448 print ('Skipping dependency patchset upload because git config '
2449 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002450 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002451 else:
2452 auth_config = auth.extract_auth_config_from_options(options)
2453 branch_cl = Changelist(branchref=local_branch, auth_config=auth_config)
2454 branch_cl_issue_url = branch_cl.GetIssueURL()
2455 branch_cl_issue = branch_cl.GetIssue()
2456 branch_cl_patchset = branch_cl.GetPatchset()
2457 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2458 upload_args.extend(
2459 ['--depends_on_patchset', '%s:%s' % (
2460 branch_cl_issue, branch_cl_patchset)])
2461 print
2462 print ('The current branch (%s) is tracking a local branch (%s) with '
2463 'an associated CL.') % (cl.GetBranch(), local_branch)
2464 print 'Adding %s/#ps%s as a dependency patchset.' % (
2465 branch_cl_issue_url, branch_cl_patchset)
2466 print
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002467
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002468 project = settings.GetProject()
2469 if project:
2470 upload_args.extend(['--project', project])
2471
rmistry@google.comef966222015-04-07 11:15:01 +00002472 if options.cq_dry_run:
2473 upload_args.extend(['--cq_dry_run'])
2474
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002475 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00002476 upload_args = ['upload'] + upload_args + args
2477 logging.info('upload.RealMain(%s)', upload_args)
2478 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00002479 issue = int(issue)
2480 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00002481 except KeyboardInterrupt:
2482 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002483 except:
2484 # If we got an exception after the user typed a description for their
2485 # change, back up the description before re-raising.
2486 if change_desc:
2487 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2488 print '\nGot exception while uploading -- saving description to %s\n' \
2489 % backup_path
2490 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002491 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002492 backup_file.close()
2493 raise
2494
2495 if not cl.GetIssue():
2496 cl.SetIssue(issue)
2497 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002498
2499 if options.use_commit_queue:
2500 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002501 return 0
2502
2503
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002504def cleanup_list(l):
2505 """Fixes a list so that comma separated items are put as individual items.
2506
2507 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
2508 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
2509 """
2510 items = sum((i.split(',') for i in l), [])
2511 stripped_items = (i.strip() for i in items)
2512 return sorted(filter(None, stripped_items))
2513
2514
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002515@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002516def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00002517 """Uploads the current changelist to codereview.
2518
2519 Can skip dependency patchset uploads for a branch by running:
2520 git config branch.branch_name.skip-deps-uploads True
2521 To unset run:
2522 git config --unset branch.branch_name.skip-deps-uploads
2523 Can also set the above globally by using the --global flag.
2524 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00002525 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2526 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002527 parser.add_option('--bypass-watchlists', action='store_true',
2528 dest='bypass_watchlists',
2529 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002530 parser.add_option('-f', action='store_true', dest='force',
2531 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002532 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00002533 parser.add_option('-t', dest='title',
2534 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002535 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002536 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002537 help='reviewer email addresses')
2538 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002539 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002540 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00002541 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00002542 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002543 parser.add_option('--emulate_svn_auto_props',
2544 '--emulate-svn-auto-props',
2545 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00002546 dest="emulate_svn_auto_props",
2547 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00002548 parser.add_option('-c', '--use-commit-queue', action='store_true',
2549 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002550 parser.add_option('--private', action='store_true',
2551 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00002552 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002553 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00002554 metavar='TARGET',
2555 help='Apply CL to remote ref TARGET. ' +
2556 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002557 parser.add_option('--squash', action='store_true',
2558 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002559 parser.add_option('--no-squash', action='store_true',
2560 help='Don\'t squash multiple commits into one ' +
2561 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002562 parser.add_option('--email', default=None,
2563 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00002564 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
2565 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00002566 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
2567 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00002568 help='Send the patchset to do a CQ dry run right after '
2569 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00002570 parser.add_option('--dependencies', action='store_true',
2571 help='Uploads CLs of all the local branches that depend on '
2572 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002573
rmistry@google.com2dd99862015-06-22 12:22:18 +00002574 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002575 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002576 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002577 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002578 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002579
sbc@chromium.org71437c02015-04-09 19:29:40 +00002580 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002581 return 1
2582
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002583 options.reviewers = cleanup_list(options.reviewers)
2584 options.cc = cleanup_list(options.cc)
2585
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002586 cl = Changelist(auth_config=auth_config)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002587 if args:
2588 # TODO(ukai): is it ok for gerrit case?
2589 base_branch = args[0]
2590 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00002591 if cl.GetBranch() is None:
2592 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
2593
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002594 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002595 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00002596 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00002597
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00002598 # Make sure authenticated to Rietveld before running expensive hooks. It is
2599 # a fast, best efforts check. Rietveld still can reject the authentication
2600 # during the actual upload.
2601 if not settings.GetIsGerrit() and auth_config.use_oauth2:
2602 authenticator = auth.get_authenticator_for_host(
2603 cl.GetRietveldServer(), auth_config)
2604 if not authenticator.has_cached_credentials():
2605 raise auth.LoginRequiredError(cl.GetRietveldServer())
2606
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002607 # Apply watchlists on upload.
2608 change = cl.GetChange(base_branch, None)
2609 watchlist = watchlists.Watchlists(change.RepositoryRoot())
2610 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002611 if not options.bypass_watchlists:
2612 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002613
ukai@chromium.orge8077812012-02-03 03:41:46 +00002614 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00002615 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002616 # Set the reviewer list now so that presubmit checks can access it.
2617 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00002618 change_description.update_reviewers(options.reviewers,
2619 options.tbr_owners,
2620 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002621 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002622 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00002623 may_prompt=not options.force,
2624 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002625 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002626 if not hook_results.should_continue():
2627 return 1
2628 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002629 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002630
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002631 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002632 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002633 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00002634 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002635 print ('The last upload made from this repository was patchset #%d but '
2636 'the most recent patchset on the server is #%d.'
2637 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00002638 print ('Uploading will still work, but if you\'ve uploaded to this issue '
2639 'from another machine or branch the patch you\'re uploading now '
2640 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002641 ask_for_data('About to upload; enter to confirm.')
2642
iannucci@chromium.org79540052012-10-19 23:15:26 +00002643 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002644 if settings.GetIsGerrit():
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002645 if options.squash and options.no_squash:
2646 DieWithError('Can only use one of --squash or --no-squash')
2647
2648 options.squash = ((settings.GetSquashGerritUploads() or options.squash) and
2649 not options.no_squash)
2650
tandrii@chromium.org1e67bb72016-02-11 12:15:49 +00002651 ret = GerritUpload(options, args, cl, change)
2652 else:
2653 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002654 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00002655 git_set_branch_value('last-upload-hash',
2656 RunGit(['rev-parse', 'HEAD']).strip())
rmistry@google.com5626a922015-02-26 14:03:30 +00002657 # Run post upload hooks, if specified.
2658 if settings.GetRunPostUploadHook():
2659 presubmit_support.DoPostUploadExecuter(
2660 change,
2661 cl,
2662 settings.GetRoot(),
2663 options.verbose,
2664 sys.stdout)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002665
rmistry@google.com2dd99862015-06-22 12:22:18 +00002666 # Upload all dependencies if specified.
2667 if options.dependencies:
2668 print
2669 print '--dependencies has been specified.'
2670 print 'All dependent local branches will be re-uploaded.'
2671 print
2672 # Remove the dependencies flag from args so that we do not end up in a
2673 # loop.
2674 orig_args.remove('--dependencies')
2675 upload_branch_deps(cl, orig_args)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002676 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00002677
2678
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002679def IsSubmoduleMergeCommit(ref):
2680 # When submodules are added to the repo, we expect there to be a single
2681 # non-git-svn merge commit at remote HEAD with a signature comment.
2682 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00002683 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002684 return RunGit(cmd) != ''
2685
2686
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002687def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002688 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002689
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002690 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002691 Updates changelog with metadata (e.g. pointer to review).
2692 Pushes/dcommits the code upstream.
2693 Updates review and closes.
2694 """
2695 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2696 help='bypass upload presubmit hook')
2697 parser.add_option('-m', dest='message',
2698 help="override review description")
2699 parser.add_option('-f', action='store_true', dest='force',
2700 help="force yes to questions (don't prompt)")
2701 parser.add_option('-c', dest='contributor',
2702 help="external contributor for patch (appended to " +
2703 "description and used as author for git). Should be " +
2704 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002705 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002706 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002707 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002708 auth_config = auth.extract_auth_config_from_options(options)
2709
2710 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002711
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002712 current = cl.GetBranch()
2713 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2714 if not settings.GetIsGitSvn() and remote == '.':
2715 print
2716 print 'Attempting to push branch %r into another local branch!' % current
2717 print
2718 print 'Either reparent this branch on top of origin/master:'
2719 print ' git reparent-branch --root'
2720 print
2721 print 'OR run `git rebase-update` if you think the parent branch is already'
2722 print 'committed.'
2723 print
2724 print ' Current parent: %r' % upstream_branch
2725 return 1
2726
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002727 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002728 # Default to merging against our best guess of the upstream branch.
2729 args = [cl.GetUpstreamBranch()]
2730
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002731 if options.contributor:
2732 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
2733 print "Please provide contibutor as 'First Last <email@example.com>'"
2734 return 1
2735
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002736 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002737 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002738
sbc@chromium.org71437c02015-04-09 19:29:40 +00002739 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002740 return 1
2741
2742 # This rev-list syntax means "show all commits not in my branch that
2743 # are in base_branch".
2744 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
2745 base_branch]).splitlines()
2746 if upstream_commits:
2747 print ('Base branch "%s" has %d commits '
2748 'not in this branch.' % (base_branch, len(upstream_commits)))
2749 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
2750 return 1
2751
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002752 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002753 svn_head = None
2754 if cmd == 'dcommit' or base_has_submodules:
2755 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
2756 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002757
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002758 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002759 # If the base_head is a submodule merge commit, the first parent of the
2760 # base_head should be a git-svn commit, which is what we're interested in.
2761 base_svn_head = base_branch
2762 if base_has_submodules:
2763 base_svn_head += '^1'
2764
2765 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002766 if extra_commits:
2767 print ('This branch has %d additional commits not upstreamed yet.'
2768 % len(extra_commits.splitlines()))
2769 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
2770 'before attempting to %s.' % (base_branch, cmd))
2771 return 1
2772
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002773 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002774 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002775 author = None
2776 if options.contributor:
2777 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002778 hook_results = cl.RunHook(
2779 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002780 may_prompt=not options.force,
2781 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002782 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002783 if not hook_results.should_continue():
2784 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002785
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002786 # Check the tree status if the tree status URL is set.
2787 status = GetTreeStatus()
2788 if 'closed' == status:
2789 print('The tree is closed. Please wait for it to reopen. Use '
2790 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2791 return 1
2792 elif 'unknown' == status:
2793 print('Unable to determine tree status. Please verify manually and '
2794 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2795 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002796
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002797 change_desc = ChangeDescription(options.message)
2798 if not change_desc.description and cl.GetIssue():
2799 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002800
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002801 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00002802 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002803 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00002804 else:
2805 print 'No description set.'
2806 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
2807 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002808
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002809 # Keep a separate copy for the commit message, because the commit message
2810 # contains the link to the Rietveld issue, while the Rietveld message contains
2811 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002812 # Keep a separate copy for the commit message.
2813 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00002814 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002815
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002816 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00002817 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00002818 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00002819 # after it. Add a period on a new line to circumvent this. Also add a space
2820 # before the period to make sure that Gitiles continues to correctly resolve
2821 # the URL.
2822 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002823 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002824 commit_desc.append_footer('Patch from %s.' % options.contributor)
2825
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00002826 print('Description:')
2827 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002828
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002829 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002830 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00002831 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002832
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002833 # We want to squash all this branch's commits into one commit with the proper
2834 # description. We do this by doing a "reset --soft" to the base branch (which
2835 # keeps the working copy the same), then dcommitting that. If origin/master
2836 # has a submodule merge commit, we'll also need to cherry-pick the squashed
2837 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002838 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002839 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
2840 # Delete the branches if they exist.
2841 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
2842 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
2843 result = RunGitWithCode(showref_cmd)
2844 if result[0] == 0:
2845 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002846
2847 # We might be in a directory that's present in this branch but not in the
2848 # trunk. Move up to the top of the tree so that git commands that expect a
2849 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002850 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002851 if rel_base_path:
2852 os.chdir(rel_base_path)
2853
2854 # Stuff our change into the merge branch.
2855 # We wrap in a try...finally block so if anything goes wrong,
2856 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002857 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002858 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002859 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002860 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002861 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00002862 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002863 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002864 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002865 RunGit(
2866 [
2867 'commit', '--author', options.contributor,
2868 '-m', commit_desc.description,
2869 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002870 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002871 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002872 if base_has_submodules:
2873 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
2874 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
2875 RunGit(['checkout', CHERRY_PICK_BRANCH])
2876 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002877 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002878 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00002879 mirror = settings.GetGitMirror(remote)
2880 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002881 pending_prefix = settings.GetPendingRefPrefix()
2882 if not pending_prefix or branch.startswith(pending_prefix):
2883 # If not using refs/pending/heads/* at all, or target ref is already set
2884 # to pending, then push to the target ref directly.
2885 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00002886 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002887 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002888 else:
2889 # Cherry-pick the change on top of pending ref and then push it.
2890 assert branch.startswith('refs/'), branch
2891 assert pending_prefix[-1] == '/', pending_prefix
2892 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00002893 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002894 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002895 if retcode == 0:
2896 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002897 else:
2898 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002899 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002900 'svn', 'dcommit',
2901 '-C%s' % options.similarity,
2902 '--no-rebase', '--rmdir',
2903 ]
2904 if settings.GetForceHttpsCommitUrl():
2905 # Allow forcing https commit URLs for some projects that don't allow
2906 # committing to http URLs (like Google Code).
2907 remote_url = cl.GetGitSvnRemoteUrl()
2908 if urlparse.urlparse(remote_url).scheme == 'http':
2909 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002910 cmd_args.append('--commit-url=%s' % remote_url)
2911 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002912 if 'Committed r' in output:
2913 revision = re.match(
2914 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2915 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002916 finally:
2917 # And then swap back to the original branch and clean up.
2918 RunGit(['checkout', '-q', cl.GetBranch()])
2919 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002920 if base_has_submodules:
2921 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002922
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002923 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002924 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002925 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002926
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002927 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002928 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002929 try:
2930 revision = WaitForRealCommit(remote, revision, base_branch, branch)
2931 # We set pushed_to_pending to False, since it made it all the way to the
2932 # real ref.
2933 pushed_to_pending = False
2934 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002935 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002936
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002937 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002938 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002939 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002940 if not to_pending:
2941 if viewvc_url and revision:
2942 change_desc.append_footer(
2943 'Committed: %s%s' % (viewvc_url, revision))
2944 elif revision:
2945 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002946 print ('Closing issue '
2947 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002948 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002949 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002950 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002951 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00002952 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00002953 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002954 if options.bypass_hooks:
2955 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2956 else:
2957 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002958 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002959 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002960
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002961 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002962 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
2963 print 'The commit is in the pending queue (%s).' % pending_ref
2964 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00002965 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002966 'footer.' % branch)
2967
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002968 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2969 if os.path.isfile(hook):
2970 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002971
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002972 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002973
2974
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002975def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
2976 print
2977 print 'Waiting for commit to be landed on %s...' % real_ref
2978 print '(If you are impatient, you may Ctrl-C once without harm)'
2979 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
2980 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00002981 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002982
2983 loop = 0
2984 while True:
2985 sys.stdout.write('fetching (%d)... \r' % loop)
2986 sys.stdout.flush()
2987 loop += 1
2988
szager@chromium.org151ebcf2016-03-09 01:08:25 +00002989 if mirror:
2990 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002991 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
2992 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2993 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
2994 for commit in commits.splitlines():
2995 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
2996 print 'Found commit on %s' % real_ref
2997 return commit
2998
2999 current_rev = to_rev
3000
3001
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003002def PushToGitPending(remote, pending_ref, upstream_ref):
3003 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
3004
3005 Returns:
3006 (retcode of last operation, output log of last operation).
3007 """
3008 assert pending_ref.startswith('refs/'), pending_ref
3009 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
3010 cherry = RunGit(['rev-parse', 'HEAD']).strip()
3011 code = 0
3012 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003013 max_attempts = 3
3014 attempts_left = max_attempts
3015 while attempts_left:
3016 if attempts_left != max_attempts:
3017 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
3018 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003019
3020 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003021 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003022 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003023 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003024 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003025 print 'Fetch failed with exit code %d.' % code
3026 if out.strip():
3027 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003028 continue
3029
3030 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003031 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003032 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003033 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003034 if code:
3035 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003036 'Your patch doesn\'t apply cleanly to ref \'%s\', '
3037 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003038 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
3039 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003040 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003041 return code, out
3042
3043 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003044 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003045 code, out = RunGitWithCode(
3046 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
3047 if code == 0:
3048 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003049 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003050 return code, out
3051
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003052 print 'Push failed with exit code %d.' % code
3053 if out.strip():
3054 print out.strip()
3055 if IsFatalPushFailure(out):
3056 print (
3057 'Fatal push error. Make sure your .netrc credentials and git '
3058 'user.email are correct and you have push access to the repo.')
3059 return code, out
3060
3061 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003062 return code, out
3063
3064
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003065def IsFatalPushFailure(push_stdout):
3066 """True if retrying push won't help."""
3067 return '(prohibited by Gerrit)' in push_stdout
3068
3069
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003070@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003071def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003072 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003073 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003074 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003075 # If it looks like previous commits were mirrored with git-svn.
3076 message = """This repository appears to be a git-svn mirror, but no
3077upstream SVN master is set. You probably need to run 'git auto-svn' once."""
3078 else:
3079 message = """This doesn't appear to be an SVN repository.
3080If your project has a true, writeable git repository, you probably want to run
3081'git cl land' instead.
3082If your project has a git mirror of an upstream SVN master, you probably need
3083to run 'git svn init'.
3084
3085Using the wrong command might cause your commit to appear to succeed, and the
3086review to be closed, without actually landing upstream. If you choose to
3087proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00003088 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00003089 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003090 return SendUpstream(parser, args, 'dcommit')
3091
3092
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003093@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003094def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003095 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003096 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003097 print('This appears to be an SVN repository.')
3098 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003099 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00003100 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003101 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003102
3103
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003104def ParseIssueNum(arg):
3105 """Parses the issue number from args if present otherwise returns None."""
3106 if re.match(r'\d+', arg):
3107 return arg
3108 if arg.startswith('http'):
3109 return re.sub(r'.*/(\d+)/?', r'\1', arg)
3110 return None
3111
3112
3113@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003114def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00003115 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003116 parser.add_option('-b', dest='newbranch',
3117 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003118 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003119 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003120 parser.add_option('-d', '--directory', action='store', metavar='DIR',
3121 help='Change to the directory DIR immediately, '
3122 'before doing anything else.')
3123 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003124 help='failed patches spew .rej files rather than '
3125 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003126 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
3127 help="don't commit after patch applies")
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003128
3129 group = optparse.OptionGroup(parser,
3130 """Options for continuing work on the current issue uploaded
3131from a different clone (e.g. different machine). Must be used independently from
3132the other options. No issue number should be specified, and the branch must have
3133an issue number associated with it""")
3134 group.add_option('--reapply', action='store_true',
3135 dest='reapply',
3136 help="""Reset the branch and reapply the issue.
3137CAUTION: This will undo any local changes in this branch""")
3138
3139 group.add_option('--pull', action='store_true', dest='pull',
3140 help="Performs a pull before reapplying.")
3141 parser.add_option_group(group)
3142
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003143 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003144 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003145 auth_config = auth.extract_auth_config_from_options(options)
3146
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003147 issue_arg = None
3148 if options.reapply :
3149 if len(args) > 0:
3150 parser.error("--reapply implies no additional arguments.")
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003151
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003152 cl = Changelist()
3153 issue_arg = cl.GetIssue()
3154 upstream = cl.GetUpstreamBranch()
3155 if upstream == None:
3156 parser.error("No upstream branch specified. Cannot reset branch")
3157
3158 RunGit(['reset', '--hard', upstream])
3159 if options.pull:
3160 RunGit(['pull'])
3161 else:
3162 if len(args) != 1:
3163 parser.error("Must specify issue number")
3164
3165 issue_arg = ParseIssueNum(args[0])
3166
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003167 # The patch URL works because ParseIssueNum won't do any substitution
3168 # as the re.sub pattern fails to match and just returns it.
3169 if issue_arg == None:
3170 parser.print_help()
3171 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003172
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003173 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003174 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003175 return 1
3176
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003177 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00003178 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003179
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003180 if options.newbranch:
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003181 if options.reapply:
3182 parser.error("--reapply excludes any option other than --pull")
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003183 if options.force:
3184 RunGit(['branch', '-D', options.newbranch],
3185 stderr=subprocess2.PIPE, error_ok=True)
3186 RunGit(['checkout', '-b', options.newbranch,
3187 Changelist().GetUpstreamBranch()])
3188
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003189 return PatchIssue(issue_arg, options.reject, options.nocommit,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003190 options.directory, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003191
3192
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003193def PatchIssue(issue_arg, reject, nocommit, directory, auth_config):
wychen@chromium.orga872e752015-04-28 23:42:18 +00003194 # PatchIssue should never be called with a dirty tree. It is up to the
3195 # caller to check this, but just in case we assert here since the
3196 # consequences of the caller not checking this could be dire.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003197 assert(not git_common.is_dirty_git_tree('apply'))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003198
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003199 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003200 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00003201 issue = int(issue_arg)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003202 cl = Changelist(issue=issue, auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003203 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00003204 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003205 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00003206 # Assume it's a URL to the patch. Default to https.
3207 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
kjellander@chromium.org44424542015-06-02 18:35:29 +00003208 match = re.match(r'(.*?)/download/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00003209 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003210 DieWithError('Must pass an issue ID or full URL for '
3211 '\'Download raw patch set\'')
kjellander@chromium.org44424542015-06-02 18:35:29 +00003212 issue = int(match.group(2))
3213 cl = Changelist(issue=issue, auth_config=auth_config)
3214 cl.rietveld_server = match.group(1)
3215 patchset = int(match.group(3))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00003216 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003217
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003218 # Switch up to the top-level directory, if necessary, in preparation for
3219 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003220 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003221 if top:
3222 os.chdir(top)
3223
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003224 # Git patches have a/ at the beginning of source paths. We strip that out
3225 # with a sed script rather than the -p flag to patch so we can feed either
3226 # Git or svn-style patches into the same apply command.
3227 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003228 try:
3229 patch_data = subprocess2.check_output(
3230 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
3231 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003232 DieWithError('Git patch mungling failed.')
3233 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003234
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003235 # We use "git apply" to apply the patch instead of "patch" so that we can
3236 # pick up file adds.
3237 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003238 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003239 if directory:
3240 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003241 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003242 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003243 elif IsGitVersionAtLeast('1.7.12'):
3244 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003245 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003246 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003247 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003248 except subprocess2.CalledProcessError:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003249 print 'Failed to apply the patch'
3250 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003251
3252 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003253 if not nocommit:
wychen@chromium.org5b3bebb2015-05-28 21:41:43 +00003254 RunGit(['commit', '-m', (cl.GetDescription() + '\n\n' +
3255 'patch from issue %(i)s at patchset '
carlosk@chromium.org71284d92014-11-14 18:12:50 +00003256 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
3257 % {'i': issue, 'p': patchset})])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003258 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003259 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00003260 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00003261 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003262 else:
3263 print "Patch applied to index."
3264 return 0
3265
3266
3267def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003268 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003269 # Provide a wrapper for git svn rebase to help avoid accidental
3270 # git svn dcommit.
3271 # It's the only command that doesn't use parser at all since we just defer
3272 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003273
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003274 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003275
3276
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003277def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003278 """Fetches the tree status and returns either 'open', 'closed',
3279 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003280 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003281 if url:
3282 status = urllib2.urlopen(url).read().lower()
3283 if status.find('closed') != -1 or status == '0':
3284 return 'closed'
3285 elif status.find('open') != -1 or status == '1':
3286 return 'open'
3287 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003288 return 'unset'
3289
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003290
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003291def GetTreeStatusReason():
3292 """Fetches the tree status from a json url and returns the message
3293 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00003294 url = settings.GetTreeStatusUrl()
3295 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003296 connection = urllib2.urlopen(json_url)
3297 status = json.loads(connection.read())
3298 connection.close()
3299 return status['message']
3300
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003301
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003302def GetBuilderMaster(bot_list):
3303 """For a given builder, fetch the master from AE if available."""
3304 map_url = 'https://builders-map.appspot.com/'
3305 try:
3306 master_map = json.load(urllib2.urlopen(map_url))
3307 except urllib2.URLError as e:
3308 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
3309 (map_url, e))
3310 except ValueError as e:
3311 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
3312 if not master_map:
3313 return None, 'Failed to build master map.'
3314
3315 result_master = ''
3316 for bot in bot_list:
3317 builder = bot.split(':', 1)[0]
3318 master_list = master_map.get(builder, [])
3319 if not master_list:
3320 return None, ('No matching master for builder %s.' % builder)
3321 elif len(master_list) > 1:
3322 return None, ('The builder name %s exists in multiple masters %s.' %
3323 (builder, master_list))
3324 else:
3325 cur_master = master_list[0]
3326 if not result_master:
3327 result_master = cur_master
3328 elif result_master != cur_master:
3329 return None, 'The builders do not belong to the same master.'
3330 return result_master, None
3331
3332
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003333def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003334 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003335 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003336 status = GetTreeStatus()
3337 if 'unset' == status:
3338 print 'You must configure your tree status URL by running "git cl config".'
3339 return 2
3340
3341 print "The tree is %s" % status
3342 print
3343 print GetTreeStatusReason()
3344 if status != 'open':
3345 return 1
3346 return 0
3347
3348
maruel@chromium.org15192402012-09-06 12:38:29 +00003349def CMDtry(parser, args):
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003350 """Triggers a try job through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00003351 group = optparse.OptionGroup(parser, "Try job options")
3352 group.add_option(
3353 "-b", "--bot", action="append",
3354 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
3355 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003356 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00003357 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003358 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00003359 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003360 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00003361 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003362 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003363 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003364 "-r", "--revision",
3365 help="Revision to use for the try job; default: the "
3366 "revision will be determined by the try server; see "
3367 "its waterfall for more info")
3368 group.add_option(
3369 "-c", "--clobber", action="store_true", default=False,
3370 help="Force a clobber before building; e.g. don't do an "
3371 "incremental build")
3372 group.add_option(
3373 "--project",
3374 help="Override which project to use. Projects are defined "
3375 "server-side to define what default bot set to use")
3376 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00003377 "-p", "--property", dest="properties", action="append", default=[],
3378 help="Specify generic properties in the form -p key1=value1 -p "
3379 "key2=value2 etc (buildbucket only). The value will be treated as "
3380 "json if decodable, or as string otherwise.")
3381 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003382 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003383 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003384 "--use-rietveld", action="store_true", default=False,
3385 help="Use Rietveld to trigger try jobs.")
3386 group.add_option(
3387 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3388 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00003389 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003390 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00003391 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003392 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00003393
machenbach@chromium.org45453142015-09-15 08:45:22 +00003394 if options.use_rietveld and options.properties:
3395 parser.error('Properties can only be specified with buildbucket')
3396
3397 # Make sure that all properties are prop=value pairs.
3398 bad_params = [x for x in options.properties if '=' not in x]
3399 if bad_params:
3400 parser.error('Got properties with missing "=": %s' % bad_params)
3401
maruel@chromium.org15192402012-09-06 12:38:29 +00003402 if args:
3403 parser.error('Unknown arguments: %s' % args)
3404
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003405 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00003406 if not cl.GetIssue():
3407 parser.error('Need to upload first')
3408
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003409 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00003410 if props.get('closed'):
3411 parser.error('Cannot send tryjobs for a closed CL')
3412
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003413 if props.get('private'):
3414 parser.error('Cannot use trybots with private issue')
3415
maruel@chromium.org15192402012-09-06 12:38:29 +00003416 if not options.name:
3417 options.name = cl.GetBranch()
3418
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003419 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003420 options.master, err_msg = GetBuilderMaster(options.bot)
3421 if err_msg:
3422 parser.error('Tryserver master cannot be found because: %s\n'
3423 'Please manually specify the tryserver master'
3424 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003425
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003426 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003427 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003428 if not options.bot:
3429 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00003430
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003431 # Get try masters from PRESUBMIT.py files.
3432 masters = presubmit_support.DoGetTryMasters(
3433 change,
3434 change.LocalPaths(),
3435 settings.GetRoot(),
3436 None,
3437 None,
3438 options.verbose,
3439 sys.stdout)
3440 if masters:
3441 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00003442
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003443 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
3444 options.bot = presubmit_support.DoGetTrySlaves(
3445 change,
3446 change.LocalPaths(),
3447 settings.GetRoot(),
3448 None,
3449 None,
3450 options.verbose,
3451 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00003452
3453 if not options.bot:
3454 # Get try masters from cq.cfg if any.
3455 # TODO(tandrii): some (but very few) projects store cq.cfg in different
3456 # location.
3457 cq_cfg = os.path.join(change.RepositoryRoot(),
3458 'infra', 'config', 'cq.cfg')
3459 if os.path.exists(cq_cfg):
3460 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00003461 cq_masters = commit_queue.get_master_builder_map(
3462 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00003463 for master, builders in cq_masters.iteritems():
3464 for builder in builders:
3465 # Skip presubmit builders, because these will fail without LGTM.
3466 if 'presubmit' not in builder.lower():
3467 masters.setdefault(master, {})[builder] = ['defaulttests']
3468 if masters:
3469 return masters
3470
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003471 if not options.bot:
3472 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00003473
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003474 builders_and_tests = {}
3475 # TODO(machenbach): The old style command-line options don't support
3476 # multiple try masters yet.
3477 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
3478 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
3479
3480 for bot in old_style:
3481 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003482 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003483 elif ',' in bot:
3484 parser.error('Specify one bot per --bot flag')
3485 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00003486 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003487
3488 for bot, tests in new_style:
3489 builders_and_tests.setdefault(bot, []).extend(tests)
3490
3491 # Return a master map with one master to be backwards compatible. The
3492 # master name defaults to an empty string, which will cause the master
3493 # not to be set on rietveld (deprecated).
3494 return {options.master: builders_and_tests}
3495
3496 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00003497
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003498 for builders in masters.itervalues():
3499 if any('triggered' in b for b in builders):
3500 print >> sys.stderr, (
3501 'ERROR You are trying to send a job to a triggered bot. This type of'
3502 ' bot requires an\ninitial job from a parent (usually a builder). '
3503 'Instead send your job to the parent.\n'
3504 'Bot list: %s' % builders)
3505 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00003506
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00003507 patchset = cl.GetMostRecentPatchset()
3508 if patchset and patchset != cl.GetPatchset():
3509 print(
3510 '\nWARNING Mismatch between local config and server. Did a previous '
3511 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3512 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003513 if options.luci:
3514 trigger_luci_job(cl, masters, options)
3515 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003516 try:
3517 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
3518 except BuildbucketResponseException as ex:
3519 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00003520 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003521 except Exception as e:
3522 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3523 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
3524 e, stacktrace)
3525 return 1
3526 else:
3527 try:
3528 cl.RpcServer().trigger_distributed_try_jobs(
3529 cl.GetIssue(), patchset, options.name, options.clobber,
3530 options.revision, masters)
3531 except urllib2.HTTPError as e:
3532 if e.code == 404:
3533 print('404 from rietveld; '
3534 'did you mean to use "git try" instead of "git cl try"?')
3535 return 1
3536 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003537
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003538 for (master, builders) in sorted(masters.iteritems()):
3539 if master:
3540 print 'Master: %s' % master
3541 length = max(len(builder) for builder in builders)
3542 for builder in sorted(builders):
3543 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00003544 return 0
3545
3546
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00003547def CMDtry_results(parser, args):
3548 group = optparse.OptionGroup(parser, "Try job results options")
3549 group.add_option(
3550 "-p", "--patchset", type=int, help="patchset number if not current.")
3551 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00003552 "--print-master", action='store_true', help="print master name as well.")
3553 group.add_option(
3554 "--color", action='store_true', default=sys.stdout.isatty(),
3555 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00003556 group.add_option(
3557 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3558 help="Host of buildbucket. The default host is %default.")
3559 parser.add_option_group(group)
3560 auth.add_auth_options(parser)
3561 options, args = parser.parse_args(args)
3562 if args:
3563 parser.error('Unrecognized args: %s' % ' '.join(args))
3564
3565 auth_config = auth.extract_auth_config_from_options(options)
3566 cl = Changelist(auth_config=auth_config)
3567 if not cl.GetIssue():
3568 parser.error('Need to upload first')
3569
3570 if not options.patchset:
3571 options.patchset = cl.GetMostRecentPatchset()
3572 if options.patchset and options.patchset != cl.GetPatchset():
3573 print(
3574 '\nWARNING Mismatch between local config and server. Did a previous '
3575 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3576 'Continuing using\npatchset %s.\n' % options.patchset)
3577 try:
3578 jobs = fetch_try_jobs(auth_config, cl, options)
3579 except BuildbucketResponseException as ex:
3580 print 'Buildbucket error: %s' % ex
3581 return 1
3582 except Exception as e:
3583 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3584 print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
3585 e, stacktrace)
3586 return 1
3587 print_tryjobs(options, jobs)
3588 return 0
3589
3590
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003591@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003592def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003593 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003594 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003595 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003596 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003597
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003598 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003599 if args:
3600 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003601 branch = cl.GetBranch()
3602 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003603 cl = Changelist()
3604 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003605
3606 # Clear configured merge-base, if there is one.
3607 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003608 else:
3609 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003610 return 0
3611
3612
thestig@chromium.org00858c82013-12-02 23:08:03 +00003613def CMDweb(parser, args):
3614 """Opens the current CL in the web browser."""
3615 _, args = parser.parse_args(args)
3616 if args:
3617 parser.error('Unrecognized args: %s' % ' '.join(args))
3618
3619 issue_url = Changelist().GetIssueURL()
3620 if not issue_url:
3621 print >> sys.stderr, 'ERROR No issue to open'
3622 return 1
3623
3624 webbrowser.open(issue_url)
3625 return 0
3626
3627
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003628def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003629 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003630 auth.add_auth_options(parser)
3631 options, args = parser.parse_args(args)
3632 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003633 if args:
3634 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003635 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003636 props = cl.GetIssueProperties()
3637 if props.get('private'):
3638 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003639 cl.SetFlag('commit', '1')
3640 return 0
3641
3642
groby@chromium.org411034a2013-02-26 15:12:01 +00003643def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003644 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003645 auth.add_auth_options(parser)
3646 options, args = parser.parse_args(args)
3647 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00003648 if args:
3649 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003650 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00003651 # Ensure there actually is an issue to close.
3652 cl.GetDescription()
3653 cl.CloseIssue()
3654 return 0
3655
3656
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003657def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003658 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003659 auth.add_auth_options(parser)
3660 options, args = parser.parse_args(args)
3661 auth_config = auth.extract_auth_config_from_options(options)
3662 if args:
3663 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003664
3665 # Uncommitted (staged and unstaged) changes will be destroyed by
3666 # "git reset --hard" if there are merging conflicts in PatchIssue().
3667 # Staged changes would be committed along with the patch from last
3668 # upload, hence counted toward the "last upload" side in the final
3669 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003670 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003671 return 1
3672
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003673 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003674 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003675 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003676 if not issue:
3677 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003678 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003679 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003680
3681 # Create a new branch based on the merge-base
3682 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
3683 try:
3684 # Patch in the latest changes from rietveld.
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003685 rtn = PatchIssue(issue, False, False, None, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003686 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003687 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003688 return rtn
3689
wychen@chromium.org06928532015-02-03 02:11:29 +00003690 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003691 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00003692 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003693 finally:
3694 RunGit(['checkout', '-q', branch])
3695 RunGit(['branch', '-D', TMP_BRANCH])
3696
3697 return 0
3698
3699
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003700def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003701 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003702 parser.add_option(
3703 '--no-color',
3704 action='store_true',
3705 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003706 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003707 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003708 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003709
3710 author = RunGit(['config', 'user.email']).strip() or None
3711
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003712 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003713
3714 if args:
3715 if len(args) > 1:
3716 parser.error('Unknown args')
3717 base_branch = args[0]
3718 else:
3719 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003720 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003721
3722 change = cl.GetChange(base_branch, None)
3723 return owners_finder.OwnersFinder(
3724 [f.LocalPath() for f in
3725 cl.GetChange(base_branch, None).AffectedFiles()],
3726 change.RepositoryRoot(), author,
3727 fopen=file, os_path=os.path, glob=glob.glob,
3728 disable_color=options.no_color).run()
3729
3730
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003731def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003732 """Generates a diff command."""
3733 # Generate diff for the current branch's changes.
3734 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
3735 upstream_commit, '--' ]
3736
3737 if args:
3738 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003739 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003740 diff_cmd.append(arg)
3741 else:
3742 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003743
3744 return diff_cmd
3745
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003746def MatchingFileType(file_name, extensions):
3747 """Returns true if the file name ends with one of the given extensions."""
3748 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003749
enne@chromium.org555cfe42014-01-29 18:21:39 +00003750@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003751def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003752 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00003753 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00003754 GN_EXTS = ['.gn', '.gni']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003755 parser.add_option('--full', action='store_true',
3756 help='Reformat the full content of all touched files')
3757 parser.add_option('--dry-run', action='store_true',
3758 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003759 parser.add_option('--python', action='store_true',
3760 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003761 parser.add_option('--diff', action='store_true',
3762 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003763 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003764
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003765 # git diff generates paths against the root of the repository. Change
3766 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003767 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003768 if rel_base_path:
3769 os.chdir(rel_base_path)
3770
digit@chromium.org29e47272013-05-17 17:01:46 +00003771 # Grab the merge-base commit, i.e. the upstream commit of the current
3772 # branch when it was created or the last time it was rebased. This is
3773 # to cover the case where the user may have called "git fetch origin",
3774 # moving the origin branch to a newer commit, but hasn't rebased yet.
3775 upstream_commit = None
3776 cl = Changelist()
3777 upstream_branch = cl.GetUpstreamBranch()
3778 if upstream_branch:
3779 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
3780 upstream_commit = upstream_commit.strip()
3781
3782 if not upstream_commit:
3783 DieWithError('Could not find base commit for this branch. '
3784 'Are you in detached state?')
3785
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003786 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
3787 diff_output = RunGit(changed_files_cmd)
3788 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00003789 # Filter out files deleted by this CL
3790 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003791
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003792 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
3793 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
3794 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00003795 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00003796
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003797 top_dir = os.path.normpath(
3798 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
3799
3800 # Locate the clang-format binary in the checkout
3801 try:
3802 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
3803 except clang_format.NotFoundError, e:
3804 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00003805
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003806 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
3807 # formatted. This is used to block during the presubmit.
3808 return_value = 0
3809
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003810 if clang_diff_files:
3811 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003812 cmd = [clang_format_tool]
3813 if not opts.dry_run and not opts.diff:
3814 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003815 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003816 if opts.diff:
3817 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003818 else:
3819 env = os.environ.copy()
3820 env['PATH'] = str(os.path.dirname(clang_format_tool))
3821 try:
3822 script = clang_format.FindClangFormatScriptInChromiumTree(
3823 'clang-format-diff.py')
3824 except clang_format.NotFoundError, e:
3825 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003826
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003827 cmd = [sys.executable, script, '-p0']
3828 if not opts.dry_run and not opts.diff:
3829 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003830
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003831 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
3832 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003833
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003834 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
3835 if opts.diff:
3836 sys.stdout.write(stdout)
3837 if opts.dry_run and len(stdout) > 0:
3838 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003839
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003840 # Similar code to above, but using yapf on .py files rather than clang-format
3841 # on C/C++ files
3842 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003843 yapf_tool = gclient_utils.FindExecutable('yapf')
3844 if yapf_tool is None:
3845 DieWithError('yapf not found in PATH')
3846
3847 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003848 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003849 cmd = [yapf_tool]
3850 if not opts.dry_run and not opts.diff:
3851 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003852 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003853 if opts.diff:
3854 sys.stdout.write(stdout)
3855 else:
3856 # TODO(sbc): yapf --lines mode still has some issues.
3857 # https://github.com/google/yapf/issues/154
3858 DieWithError('--python currently only works with --full')
3859
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003860 # Dart's formatter does not have the nice property of only operating on
3861 # modified chunks, so hard code full.
3862 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003863 try:
3864 command = [dart_format.FindDartFmtToolInChromiumTree()]
3865 if not opts.dry_run and not opts.diff:
3866 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003867 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003868
ppi@chromium.org6593d932016-03-03 15:41:15 +00003869 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003870 if opts.dry_run and stdout:
3871 return_value = 2
3872 except dart_format.NotFoundError as e:
erikcorry@chromium.org3e445022015-12-17 09:07:26 +00003873 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
3874 'found in this checkout. Files in other languages are still ' +
3875 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003876
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00003877 # Format GN build files. Always run on full build files for canonical form.
3878 if gn_diff_files:
3879 cmd = ['gn', 'format']
3880 if not opts.dry_run and not opts.diff:
3881 cmd.append('--in-place')
3882 for gn_diff_file in gn_diff_files:
3883 stdout = RunCommand(cmd + [gn_diff_file], cwd=top_dir)
3884 if opts.diff:
3885 sys.stdout.write(stdout)
3886
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003887 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003888
3889
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00003890@subcommand.usage('<codereview url or issue id>')
3891def CMDcheckout(parser, args):
3892 """Checks out a branch associated with a given Rietveld issue."""
3893 _, args = parser.parse_args(args)
3894
3895 if len(args) != 1:
3896 parser.print_help()
3897 return 1
3898
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003899 target_issue = ParseIssueNum(args[0])
3900 if target_issue == None:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00003901 parser.print_help()
3902 return 1
3903
3904 key_and_issues = [x.split() for x in RunGit(
3905 ['config', '--local', '--get-regexp', r'branch\..*\.rietveldissue'])
3906 .splitlines()]
3907 branches = []
3908 for key, issue in key_and_issues:
3909 if issue == target_issue:
3910 branches.append(re.sub(r'branch\.(.*)\.rietveldissue', r'\1', key))
3911
3912 if len(branches) == 0:
3913 print 'No branch found for issue %s.' % target_issue
3914 return 1
3915 if len(branches) == 1:
3916 RunGit(['checkout', branches[0]])
3917 else:
3918 print 'Multiple branches match issue %s:' % target_issue
3919 for i in range(len(branches)):
3920 print '%d: %s' % (i, branches[i])
3921 which = raw_input('Choose by index: ')
3922 try:
3923 RunGit(['checkout', branches[int(which)]])
3924 except (IndexError, ValueError):
3925 print 'Invalid selection, not checking out any branch.'
3926 return 1
3927
3928 return 0
3929
3930
maruel@chromium.org29404b52014-09-08 22:58:00 +00003931def CMDlol(parser, args):
3932 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00003933 print zlib.decompress(base64.b64decode(
3934 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
3935 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
3936 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
3937 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00003938 return 0
3939
3940
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003941class OptionParser(optparse.OptionParser):
3942 """Creates the option parse and add --verbose support."""
3943 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003944 optparse.OptionParser.__init__(
3945 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003946 self.add_option(
3947 '-v', '--verbose', action='count', default=0,
3948 help='Use 2 times for more debugging info')
3949
3950 def parse_args(self, args=None, values=None):
3951 options, args = optparse.OptionParser.parse_args(self, args, values)
3952 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
3953 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
3954 return options, args
3955
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003956
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003957def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00003958 if sys.hexversion < 0x02060000:
3959 print >> sys.stderr, (
3960 '\nYour python version %s is unsupported, please upgrade.\n' %
3961 sys.version.split(' ', 1)[0])
3962 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003963
maruel@chromium.orgddd59412011-11-30 14:20:38 +00003964 # Reload settings.
3965 global settings
3966 settings = Settings()
3967
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003968 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003969 dispatcher = subcommand.CommandDispatcher(__name__)
3970 try:
3971 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003972 except auth.AuthenticationError as e:
3973 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003974 except urllib2.HTTPError, e:
3975 if e.code != 500:
3976 raise
3977 DieWithError(
3978 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
3979 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00003980 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003981
3982
3983if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003984 # These affect sys.stdout so do it outside of main() to simplify mocks in
3985 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00003986 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003987 colorama.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00003988 try:
3989 sys.exit(main(sys.argv[1:]))
3990 except KeyboardInterrupt:
3991 sys.stderr.write('interrupted\n')
3992 sys.exit(1)