blob: 59e412123fcd73404fddc29c1e6fd0245c66440c [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
dnj@chromium.orgf585b1f2016-03-28 22:25:16 +00008"""A git-command for integrating reviews on Rietveld."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000010from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000011from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000012import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000013import collections
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000014import glob
sheyang@google.com6ebaf782015-05-12 19:17:54 +000015import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000016import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import logging
18import optparse
19import os
maruel@chromium.org1033efd2013-07-23 23:25:09 +000020import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000022import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000023import sys
bauerb@chromium.org27386dd2015-02-16 10:45:39 +000024import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025import textwrap
sheyang@google.com6ebaf782015-05-12 19:17:54 +000026import time
27import traceback
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000028import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000030import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000031import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000032import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000033import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000034
35try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000036 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000037except ImportError:
38 pass
39
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000040from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000041from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000043import auth
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +000044from luci_hacks import trigger_luci_job as luci_trigger
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000045import clang_format
tandrii@chromium.org71184c02016-01-13 15:18:44 +000046import commit_queue
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000047import dart_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000048import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000049import gclient_utils
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):
dnj@chromium.orgf585b1f2016-03-28 22:25:16 +0000159 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):
dnj@chromium.orgf585b1f2016-03-28 22:25:16 +0000171 branch = Changelist().GetBranch()
iannucci@chromium.org79540052012-10-19 23:15:26 +0000172 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'."""
dnj@chromium.orgf585b1f2016-03-28 22:25:16 +0000816 return branch.replace('refs/heads/', '')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000817
818
819class Changelist(object):
dnj@chromium.orgf585b1f2016-03-28 22:25:16 +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()
dnj@chromium.orgf585b1f2016-03-28 22:25:16 +0000826 settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000827 self.branchref = branchref
828 if self.branchref:
829 self.branch = ShortBranchName(self.branchref)
830 else:
831 self.branch = None
dnj@chromium.orgf585b1f2016-03-28 22:25:16 +0000832 self.rietveld_server = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000833 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 = ()
dnj@chromium.orgf585b1f2016-03-28 22:25:16 +0000842 self._auth_config = auth_config
843 self._props = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000844 self._remote = None
dnj@chromium.orgf585b1f2016-03-28 22:25:16 +0000845 self._rpc_server = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000846
dnj@chromium.orgf585b1f2016-03-28 22:25:16 +0000847 @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:
dnj@chromium.orgf585b1f2016-03-28 22:25:16 +0000877 branchref = RunGit(['symbolic-ref', 'HEAD'],
878 stderr=subprocess2.VOID, error_ok=True).strip()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000879 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:
dnj@chromium.orgf585b1f2016-03-28 22:25:16 +0000922 DieWithError("""Unable to determine default branch to diff against.
923Either 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 ...").""")
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000927
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
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001065 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001066 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001067 if self.issue is None and not self.lookedup_issue:
dnj@chromium.orgf585b1f2016-03-28 22:25:16 +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
dnj@chromium.orgf585b1f2016-03-28 22:25:16 +00001073 def GetRietveldServer(self):
1074 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():
1078 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())
1082 if not self.rietveld_server:
1083 self.rietveld_server = settings.GetDefaultServerUrl()
1084 return self.rietveld_server
1085
1086 def GetGerritServer(self):
1087 # We don't support multiple Gerrit servers, and assume it to be same as
1088 # origin, except with a '-review' suffix for first subdomain.
1089 parts = urlparse.urlparse(self.GetRemoteUrl()).netloc.split('.')
1090 parts[0] = parts[0] + '-review'
1091 return 'https://%s' % '.'.join(parts)
1092
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001093 def GetIssueURL(self):
1094 """Get the URL for a particular issue."""
dnj@chromium.orgf585b1f2016-03-28 22:25:16 +00001095 if not self.GetIssue():
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001096 return None
dnj@chromium.orgf585b1f2016-03-28 22:25:16 +00001097 if settings.GetIsGerrit():
1098 return '%s/%s' % (self.GetGerritServer(), self.GetIssue())
1099 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001100
1101 def GetDescription(self, pretty=False):
1102 if not self.has_description:
1103 if self.GetIssue():
dnj@chromium.orgf585b1f2016-03-28 22:25:16 +00001104 issue = self.GetIssue()
1105 try:
1106 self.description = self.RpcServer().get_description(issue).strip()
1107 except urllib2.HTTPError as e:
1108 if e.code == 404:
1109 DieWithError(
1110 ('\nWhile fetching the description for issue %d, received a '
1111 '404 (not found)\n'
1112 'error. It is likely that you deleted this '
1113 'issue on the server. If this is the\n'
1114 'case, please run\n\n'
1115 ' git cl issue 0\n\n'
1116 'to clear the association with the deleted issue. Then run '
1117 'this command again.') % issue)
1118 else:
1119 DieWithError(
1120 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1121 except urllib2.URLError as e:
1122 print >> sys.stderr, (
1123 'Warning: Failed to retrieve CL description due to network '
1124 'failure.')
1125 self.description = ''
1126
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001127 self.has_description = True
1128 if pretty:
1129 wrapper = textwrap.TextWrapper()
1130 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1131 return wrapper.fill(self.description)
1132 return self.description
1133
1134 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001135 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001136 if self.patchset is None and not self.lookedup_patchset:
dnj@chromium.orgf585b1f2016-03-28 22:25:16 +00001137 patchset = RunGit(['config', self._PatchsetSetting()],
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001138 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001139 self.patchset = int(patchset) or None if patchset else None
1140 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001141 return self.patchset
1142
1143 def SetPatchset(self, patchset):
1144 """Set this branch's patchset. If patchset=0, clears the patchset."""
1145 if patchset:
dnj@chromium.orgf585b1f2016-03-28 22:25:16 +00001146 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001147 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001148 else:
dnj@chromium.orgf585b1f2016-03-28 22:25:16 +00001149 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001150 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001151 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001152
dnj@chromium.orgf585b1f2016-03-28 22:25:16 +00001153 def GetMostRecentPatchset(self):
1154 return self.GetIssueProperties()['patchsets'][-1]
1155
1156 def GetPatchSetDiff(self, issue, patchset):
1157 return self.RpcServer().get(
1158 '/download/issue%s_%s.diff' % (issue, patchset))
1159
1160 def GetIssueProperties(self):
1161 if self._props is None:
1162 issue = self.GetIssue()
1163 if not issue:
1164 self._props = {}
1165 else:
1166 self._props = self.RpcServer().get_issue_properties(issue, True)
1167 return self._props
1168
1169 def GetApprovingReviewers(self):
1170 return get_approving_reviewers(self.GetIssueProperties())
1171
1172 def AddComment(self, message):
1173 return self.RpcServer().add_comment(self.GetIssue(), message)
1174
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001175 def SetIssue(self, issue=None):
1176 """Set this branch's issue. If issue isn't given, clears the issue."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001177 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001178 self.issue = issue
dnj@chromium.orgf585b1f2016-03-28 22:25:16 +00001179 RunGit(['config', self._IssueSetting(), str(issue)])
1180 if not settings.GetIsGerrit() and self.rietveld_server:
1181 RunGit(['config', self._RietveldServer(), self.rietveld_server])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001182 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +00001183 current_issue = self.GetIssue()
1184 if current_issue:
dnj@chromium.orgf585b1f2016-03-28 22:25:16 +00001185 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001186 self.issue = None
1187 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001188
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001189 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001190 if not self.GitSanityChecks(upstream_branch):
1191 DieWithError('\nGit sanity check failure')
1192
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001193 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001194 if not root:
1195 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001196 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001197
1198 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001199 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001200 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001201 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001202 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001203 except subprocess2.CalledProcessError:
1204 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001205 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001206 'This branch probably doesn\'t exist anymore. To reset the\n'
1207 'tracking branch, please run\n'
1208 ' git branch --set-upstream %s trunk\n'
1209 'replacing trunk with origin/master or the relevant branch') %
1210 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001211
maruel@chromium.org52424302012-08-29 15:14:30 +00001212 issue = self.GetIssue()
1213 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001214 if issue:
1215 description = self.GetDescription()
1216 else:
1217 # If the change was never uploaded, use the log messages of all commits
1218 # up to the branch point, as git cl upload will prefill the description
1219 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001220 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1221 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001222
1223 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001224 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001225 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001226 name,
1227 description,
1228 absroot,
1229 files,
1230 issue,
1231 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001232 author,
1233 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001234
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001235 def GetStatus(self):
1236 """Apply a rough heuristic to give a simple summary of an issue's review
1237 or CQ status, assuming adherence to a common workflow.
1238
1239 Returns None if no issue for this branch, or one of the following keywords:
1240 * 'error' - error from review tool (including deleted issues)
1241 * 'unsent' - not sent for review
1242 * 'waiting' - waiting for review
1243 * 'reply' - waiting for owner to reply to review
1244 * 'lgtm' - LGTM from at least one approved reviewer
1245 * 'commit' - in the commit queue
1246 * 'closed' - closed
1247 """
1248 if not self.GetIssue():
1249 return None
1250
1251 try:
1252 props = self.GetIssueProperties()
1253 except urllib2.HTTPError:
1254 return 'error'
1255
1256 if props.get('closed'):
1257 # Issue is closed.
1258 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001259 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001260 # Issue is in the commit queue.
1261 return 'commit'
1262
1263 try:
1264 reviewers = self.GetApprovingReviewers()
1265 except urllib2.HTTPError:
1266 return 'error'
1267
1268 if reviewers:
1269 # Was LGTM'ed.
1270 return 'lgtm'
1271
1272 messages = props.get('messages') or []
1273
1274 if not messages:
1275 # No message was sent.
1276 return 'unsent'
1277 if messages[-1]['sender'] != props.get('owner_email'):
1278 # Non-LGTM reply from non-owner
1279 return 'reply'
1280 return 'waiting'
1281
dnj@chromium.orgf585b1f2016-03-28 22:25:16 +00001282 def RunHook(self, committing, may_prompt, verbose, change):
1283 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1284
1285 try:
1286 return presubmit_support.DoPresubmitChecks(change, committing,
1287 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1288 default_presubmit=None, may_prompt=may_prompt,
1289 rietveld_obj=self.RpcServer())
1290 except presubmit_support.PresubmitFailure, e:
1291 DieWithError(
1292 ('%s\nMaybe your depot_tools is out of date?\n'
1293 'If all fails, contact maruel@') % e)
1294
1295 def UpdateDescription(self, description):
1296 self.description = description
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001297 return self.RpcServer().update_description(
1298 self.GetIssue(), self.description)
1299
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001300 def CloseIssue(self):
dnj@chromium.orgf585b1f2016-03-28 22:25:16 +00001301 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001302 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001303
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001304 def SetFlag(self, flag, value):
1305 """Patchset must match."""
1306 if not self.GetPatchset():
1307 DieWithError('The patchset needs to match. Send another patchset.')
1308 try:
1309 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001310 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001311 except urllib2.HTTPError, e:
1312 if e.code == 404:
1313 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1314 if e.code == 403:
1315 DieWithError(
1316 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1317 'match?') % (self.GetIssue(), self.GetPatchset()))
1318 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001319
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001320 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001321 """Returns an upload.RpcServer() to access this review's rietveld instance.
1322 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001323 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001324 self._rpc_server = rietveld.CachingRietveld(
dnj@chromium.orgf585b1f2016-03-28 22:25:16 +00001325 self.GetRietveldServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001326 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001327 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001328
dnj@chromium.orgf585b1f2016-03-28 22:25:16 +00001329 def _IssueSetting(self):
1330 """Return the git setting that stores this change's issue."""
1331 return 'branch.%s.rietveldissue' % self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001332
dnj@chromium.orgf585b1f2016-03-28 22:25:16 +00001333 def _PatchsetSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001334 """Return the git setting that stores this change's most recent patchset."""
1335 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1336
dnj@chromium.orgf585b1f2016-03-28 22:25:16 +00001337 def _RietveldServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001338 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001339 branch = self.GetBranch()
1340 if branch:
1341 return 'branch.%s.rietveldserver' % branch
1342 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001343
1344
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001345class ChangeDescription(object):
1346 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001347 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00001348 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001349
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001350 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00001351 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001352
agable@chromium.org42c20792013-09-12 17:34:49 +00001353 @property # www.logilab.org/ticket/89786
1354 def description(self): # pylint: disable=E0202
1355 return '\n'.join(self._description_lines)
1356
1357 def set_description(self, desc):
1358 if isinstance(desc, basestring):
1359 lines = desc.splitlines()
1360 else:
1361 lines = [line.rstrip() for line in desc]
1362 while lines and not lines[0]:
1363 lines.pop(0)
1364 while lines and not lines[-1]:
1365 lines.pop(-1)
1366 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001367
piman@chromium.org336f9122014-09-04 02:16:55 +00001368 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001369 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001370 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001371 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001372 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001373 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001374
agable@chromium.org42c20792013-09-12 17:34:49 +00001375 # Get the set of R= and TBR= lines and remove them from the desciption.
1376 regexp = re.compile(self.R_LINE)
1377 matches = [regexp.match(line) for line in self._description_lines]
1378 new_desc = [l for i, l in enumerate(self._description_lines)
1379 if not matches[i]]
1380 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001381
agable@chromium.org42c20792013-09-12 17:34:49 +00001382 # Construct new unified R= and TBR= lines.
1383 r_names = []
1384 tbr_names = []
1385 for match in matches:
1386 if not match:
1387 continue
1388 people = cleanup_list([match.group(2).strip()])
1389 if match.group(1) == 'TBR':
1390 tbr_names.extend(people)
1391 else:
1392 r_names.extend(people)
1393 for name in r_names:
1394 if name not in reviewers:
1395 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001396 if add_owners_tbr:
1397 owners_db = owners.Database(change.RepositoryRoot(),
1398 fopen=file, os_path=os.path, glob=glob.glob)
1399 all_reviewers = set(tbr_names + reviewers)
1400 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1401 all_reviewers)
1402 tbr_names.extend(owners_db.reviewers_for(missing_files,
1403 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001404 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1405 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1406
1407 # Put the new lines in the description where the old first R= line was.
1408 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1409 if 0 <= line_loc < len(self._description_lines):
1410 if new_tbr_line:
1411 self._description_lines.insert(line_loc, new_tbr_line)
1412 if new_r_line:
1413 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001414 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001415 if new_r_line:
1416 self.append_footer(new_r_line)
1417 if new_tbr_line:
1418 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001419
1420 def prompt(self):
1421 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001422 self.set_description([
1423 '# Enter a description of the change.',
1424 '# This will be displayed on the codereview site.',
1425 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00001426 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00001427 '--------------------',
1428 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001429
agable@chromium.org42c20792013-09-12 17:34:49 +00001430 regexp = re.compile(self.BUG_LINE)
1431 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00001432 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00001433 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001434 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001435 if not content:
1436 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001437 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001438
1439 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001440 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1441 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001442 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001443 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001444
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001445 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001446 if self._description_lines:
1447 # Add an empty line if either the last line or the new line isn't a tag.
1448 last_line = self._description_lines[-1]
1449 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1450 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1451 self._description_lines.append('')
1452 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001453
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001454 def get_reviewers(self):
1455 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001456 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1457 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001458 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001459
1460
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001461def get_approving_reviewers(props):
1462 """Retrieves the reviewers that approved a CL from the issue properties with
1463 messages.
1464
1465 Note that the list may contain reviewers that are not committer, thus are not
1466 considered by the CQ.
1467 """
1468 return sorted(
1469 set(
1470 message['sender']
1471 for message in props['messages']
1472 if message['approval'] and message['sender'] in props['reviewers']
1473 )
1474 )
1475
1476
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001477def FindCodereviewSettingsFile(filename='codereview.settings'):
1478 """Finds the given file starting in the cwd and going up.
1479
1480 Only looks up to the top of the repository unless an
1481 'inherit-review-settings-ok' file exists in the root of the repository.
1482 """
1483 inherit_ok_file = 'inherit-review-settings-ok'
1484 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001485 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001486 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1487 root = '/'
1488 while True:
1489 if filename in os.listdir(cwd):
1490 if os.path.isfile(os.path.join(cwd, filename)):
1491 return open(os.path.join(cwd, filename))
1492 if cwd == root:
1493 break
1494 cwd = os.path.dirname(cwd)
1495
1496
1497def LoadCodereviewSettingsFromFile(fileobj):
1498 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001499 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001500
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001501 def SetProperty(name, setting, unset_error_ok=False):
1502 fullname = 'rietveld.' + name
1503 if setting in keyvals:
1504 RunGit(['config', fullname, keyvals[setting]])
1505 else:
1506 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1507
1508 SetProperty('server', 'CODE_REVIEW_SERVER')
1509 # Only server setting is required. Other settings can be absent.
1510 # In that case, we ignore errors raised during option deletion attempt.
1511 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001512 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001513 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1514 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001515 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001516 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001517 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
1518 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001519 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001520 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001521 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00001522 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
1523 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001524
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001525 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001526 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001527
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00001528 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
1529 RunGit(['config', 'gerrit.squash-uploads',
1530 keyvals['GERRIT_SQUASH_UPLOADS']])
1531
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001532 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1533 #should be of the form
1534 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1535 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1536 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1537 keyvals['ORIGIN_URL_CONFIG']])
1538
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001539
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001540def urlretrieve(source, destination):
1541 """urllib is broken for SSL connections via a proxy therefore we
1542 can't use urllib.urlretrieve()."""
1543 with open(destination, 'w') as f:
1544 f.write(urllib2.urlopen(source).read())
1545
1546
ukai@chromium.org712d6102013-11-27 00:52:58 +00001547def hasSheBang(fname):
1548 """Checks fname is a #! script."""
1549 with open(fname) as f:
1550 return f.read(2).startswith('#!')
1551
1552
tandrii@chromium.org18630d62016-03-04 12:06:02 +00001553def DownloadGerritHook(force):
1554 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001555
1556 Args:
1557 force: True to update hooks. False to install hooks if not present.
1558 """
1559 if not settings.GetIsGerrit():
1560 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001561 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001562 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1563 if not os.access(dst, os.X_OK):
1564 if os.path.exists(dst):
1565 if not force:
1566 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001567 try:
tandrii@chromium.org18630d62016-03-04 12:06:02 +00001568 print(
1569 'WARNING: installing Gerrit commit-msg hook.\n'
1570 ' This behavior of git cl will soon be disabled.\n'
1571 ' See bug http://crbug.com/579176.')
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001572 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001573 if not hasSheBang(dst):
1574 DieWithError('Not a script: %s\n'
1575 'You need to download from\n%s\n'
1576 'into .git/hooks/commit-msg and '
1577 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001578 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1579 except Exception:
1580 if os.path.exists(dst):
1581 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001582 DieWithError('\nFailed to download hooks.\n'
1583 'You need to download from\n%s\n'
1584 'into .git/hooks/commit-msg and '
1585 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001586
1587
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00001588
1589def GetRietveldCodereviewSettingsInteractively():
1590 """Prompt the user for settings."""
1591 server = settings.GetDefaultServerUrl(error_ok=True)
1592 prompt = 'Rietveld server (host[:port])'
1593 prompt += ' [%s]' % (server or DEFAULT_SERVER)
1594 newserver = ask_for_data(prompt + ':')
1595 if not server and not newserver:
1596 newserver = DEFAULT_SERVER
1597 if newserver:
1598 newserver = gclient_utils.UpgradeToHttps(newserver)
1599 if newserver != server:
1600 RunGit(['config', 'rietveld.server', newserver])
1601
1602 def SetProperty(initial, caption, name, is_url):
1603 prompt = caption
1604 if initial:
1605 prompt += ' ("x" to clear) [%s]' % initial
1606 new_val = ask_for_data(prompt + ':')
1607 if new_val == 'x':
1608 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
1609 elif new_val:
1610 if is_url:
1611 new_val = gclient_utils.UpgradeToHttps(new_val)
1612 if new_val != initial:
1613 RunGit(['config', 'rietveld.' + name, new_val])
1614
1615 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
1616 SetProperty(settings.GetDefaultPrivateFlag(),
1617 'Private flag (rietveld only)', 'private', False)
1618 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
1619 'tree-status-url', False)
1620 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
1621 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
1622 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
1623 'run-post-upload-hook', False)
1624
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001625@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001626def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001627 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001628
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00001629 print('WARNING: git cl config works for Rietveld only.\n'
1630 'For Gerrit, see http://crbug.com/579160.')
1631 # TODO(tandrii): add Gerrit support as part of http://crbug.com/579160.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001632 parser.add_option('--activate-update', action='store_true',
1633 help='activate auto-updating [rietveld] section in '
1634 '.git/config')
1635 parser.add_option('--deactivate-update', action='store_true',
1636 help='deactivate auto-updating [rietveld] section in '
1637 '.git/config')
1638 options, args = parser.parse_args(args)
1639
1640 if options.deactivate_update:
1641 RunGit(['config', 'rietveld.autoupdate', 'false'])
1642 return
1643
1644 if options.activate_update:
1645 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1646 return
1647
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001648 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00001649 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001650 return 0
1651
1652 url = args[0]
1653 if not url.endswith('codereview.settings'):
1654 url = os.path.join(url, 'codereview.settings')
1655
1656 # Load code review settings and download hooks (if available).
1657 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
1658 return 0
1659
1660
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001661def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001662 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001663 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1664 branch = ShortBranchName(branchref)
1665 _, args = parser.parse_args(args)
1666 if not args:
1667 print("Current base-url:")
1668 return RunGit(['config', 'branch.%s.base-url' % branch],
1669 error_ok=False).strip()
1670 else:
1671 print("Setting base-url to %s" % args[0])
1672 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1673 error_ok=False).strip()
1674
1675
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001676def color_for_status(status):
1677 """Maps a Changelist status to color, for CMDstatus and other tools."""
1678 return {
1679 'unsent': Fore.RED,
1680 'waiting': Fore.BLUE,
1681 'reply': Fore.YELLOW,
1682 'lgtm': Fore.GREEN,
1683 'commit': Fore.MAGENTA,
1684 'closed': Fore.CYAN,
1685 'error': Fore.WHITE,
1686 }.get(status, Fore.WHITE)
1687
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001688def fetch_cl_status(branch, auth_config=None):
1689 """Fetches information for an issue and returns (branch, issue, status)."""
1690 cl = Changelist(branchref=branch, auth_config=auth_config)
1691 url = cl.GetIssueURL()
1692 status = cl.GetStatus()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001693
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001694 if url and (not status or status == 'error'):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001695 # The issue probably doesn't exist anymore.
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001696 url += ' (broken)'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001697
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001698 return (branch, url, status)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001699
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001700def get_cl_statuses(
1701 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001702 """Returns a blocking iterable of (branch, issue, color) for given branches.
1703
1704 If fine_grained is true, this will fetch CL statuses from the server.
1705 Otherwise, simply indicate if there's a matching url for the given branches.
1706
1707 If max_processes is specified, it is used as the maximum number of processes
1708 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
1709 spawned.
1710 """
1711 # Silence upload.py otherwise it becomes unwieldly.
1712 upload.verbosity = 0
1713
1714 if fine_grained:
1715 # Process one branch synchronously to work through authentication, then
1716 # spawn processes to process all the other branches in parallel.
1717 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001718 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
1719 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001720
1721 branches_to_fetch = branches[1:]
1722 pool = ThreadPool(
1723 min(max_processes, len(branches_to_fetch))
1724 if max_processes is not None
1725 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001726 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001727 yield x
1728 else:
1729 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1730 for b in branches:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001731 cl = Changelist(branchref=b, auth_config=auth_config)
1732 url = cl.GetIssueURL()
1733 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001734
rmistry@google.com2dd99862015-06-22 12:22:18 +00001735
1736def upload_branch_deps(cl, args):
1737 """Uploads CLs of local branches that are dependents of the current branch.
1738
1739 If the local branch dependency tree looks like:
1740 test1 -> test2.1 -> test3.1
1741 -> test3.2
1742 -> test2.2 -> test3.3
1743
1744 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
1745 run on the dependent branches in this order:
1746 test2.1, test3.1, test3.2, test2.2, test3.3
1747
1748 Note: This function does not rebase your local dependent branches. Use it when
1749 you make a change to the parent branch that will not conflict with its
1750 dependent branches, and you would like their dependencies updated in
1751 Rietveld.
1752 """
1753 if git_common.is_dirty_git_tree('upload-branch-deps'):
1754 return 1
1755
1756 root_branch = cl.GetBranch()
1757 if root_branch is None:
1758 DieWithError('Can\'t find dependent branches from detached HEAD state. '
1759 'Get on a branch!')
1760 if not cl.GetIssue() or not cl.GetPatchset():
1761 DieWithError('Current branch does not have an uploaded CL. We cannot set '
1762 'patchset dependencies without an uploaded CL.')
1763
1764 branches = RunGit(['for-each-ref',
1765 '--format=%(refname:short) %(upstream:short)',
1766 'refs/heads'])
1767 if not branches:
1768 print('No local branches found.')
1769 return 0
1770
1771 # Create a dictionary of all local branches to the branches that are dependent
1772 # on it.
1773 tracked_to_dependents = collections.defaultdict(list)
1774 for b in branches.splitlines():
1775 tokens = b.split()
1776 if len(tokens) == 2:
1777 branch_name, tracked = tokens
1778 tracked_to_dependents[tracked].append(branch_name)
1779
1780 print
1781 print 'The dependent local branches of %s are:' % root_branch
1782 dependents = []
1783 def traverse_dependents_preorder(branch, padding=''):
1784 dependents_to_process = tracked_to_dependents.get(branch, [])
1785 padding += ' '
1786 for dependent in dependents_to_process:
1787 print '%s%s' % (padding, dependent)
1788 dependents.append(dependent)
1789 traverse_dependents_preorder(dependent, padding)
1790 traverse_dependents_preorder(root_branch)
1791 print
1792
1793 if not dependents:
1794 print 'There are no dependent local branches for %s' % root_branch
1795 return 0
1796
1797 print ('This command will checkout all dependent branches and run '
1798 '"git cl upload".')
1799 ask_for_data('[Press enter to continue or ctrl-C to quit]')
1800
andybons@chromium.org962f9462016-02-03 20:00:42 +00001801 # Add a default patchset title to all upload calls in Rietveld.
1802 if not settings.GetIsGerrit():
1803 args.extend(['-t', 'Updated patchset dependency'])
1804
rmistry@google.com2dd99862015-06-22 12:22:18 +00001805 # Record all dependents that failed to upload.
1806 failures = {}
1807 # Go through all dependents, checkout the branch and upload.
1808 try:
1809 for dependent_branch in dependents:
1810 print
1811 print '--------------------------------------'
1812 print 'Running "git cl upload" from %s:' % dependent_branch
1813 RunGit(['checkout', '-q', dependent_branch])
1814 print
1815 try:
1816 if CMDupload(OptionParser(), args) != 0:
1817 print 'Upload failed for %s!' % dependent_branch
1818 failures[dependent_branch] = 1
1819 except: # pylint: disable=W0702
1820 failures[dependent_branch] = 1
1821 print
1822 finally:
1823 # Swap back to the original root branch.
1824 RunGit(['checkout', '-q', root_branch])
1825
1826 print
1827 print 'Upload complete for dependent branches!'
1828 for dependent_branch in dependents:
1829 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
1830 print ' %s : %s' % (dependent_branch, upload_status)
1831 print
1832
1833 return 0
1834
1835
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001836def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001837 """Show status of changelists.
1838
1839 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001840 - Red not sent for review or broken
1841 - Blue waiting for review
1842 - Yellow waiting for you to reply to review
1843 - Green LGTM'ed
1844 - Magenta in the commit queue
1845 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001846
1847 Also see 'git cl comments'.
1848 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001849 parser.add_option('--field',
1850 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001851 parser.add_option('-f', '--fast', action='store_true',
1852 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001853 parser.add_option(
1854 '-j', '--maxjobs', action='store', type=int,
1855 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001856
1857 auth.add_auth_options(parser)
1858 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001859 if args:
1860 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001861 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001862
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001863 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001864 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001865 if options.field.startswith('desc'):
1866 print cl.GetDescription()
1867 elif options.field == 'id':
1868 issueid = cl.GetIssue()
1869 if issueid:
1870 print issueid
1871 elif options.field == 'patch':
1872 patchset = cl.GetPatchset()
1873 if patchset:
1874 print patchset
1875 elif options.field == 'url':
1876 url = cl.GetIssueURL()
1877 if url:
1878 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001879 return 0
1880
1881 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1882 if not branches:
1883 print('No local branch found.')
1884 return 0
1885
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001886 changes = (
1887 Changelist(branchref=b, auth_config=auth_config)
1888 for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001889 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001890 alignment = max(5, max(len(b) for b in branches))
1891 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001892 output = get_cl_statuses(branches,
1893 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001894 max_processes=options.maxjobs,
1895 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001896
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001897 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001898 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001899 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001900 while branch not in branch_statuses:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001901 b, i, status = output.next()
1902 branch_statuses[b] = (i, status)
1903 issue_url, status = branch_statuses.pop(branch)
1904 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001905 reset = Fore.RESET
1906 if not sys.stdout.isatty():
1907 color = ''
1908 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001909 status_str = '(%s)' % status if status else ''
1910 print ' %*s : %s%s %s%s' % (
1911 alignment, ShortBranchName(branch), color, issue_url, status_str,
1912 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001913
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001914 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001915 print
1916 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001917 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00001918 if not cl.GetIssue():
1919 print 'No issue assigned.'
1920 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001921 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00001922 if not options.fast:
1923 print 'Issue description:'
1924 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001925 return 0
1926
1927
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001928def colorize_CMDstatus_doc():
1929 """To be called once in main() to add colors to git cl status help."""
1930 colors = [i for i in dir(Fore) if i[0].isupper()]
1931
1932 def colorize_line(line):
1933 for color in colors:
1934 if color in line.upper():
1935 # Extract whitespaces first and the leading '-'.
1936 indent = len(line) - len(line.lstrip(' ')) + 1
1937 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1938 return line
1939
1940 lines = CMDstatus.__doc__.splitlines()
1941 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1942
1943
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001944@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001945def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001946 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001947
1948 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001949 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00001950 parser.add_option('-r', '--reverse', action='store_true',
1951 help='Lookup the branch(es) for the specified issues. If '
1952 'no issues are specified, all branches with mapped '
1953 'issues will be listed.')
1954 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001955
dnj@chromium.org406c4402015-03-03 17:22:28 +00001956 if options.reverse:
1957 branches = RunGit(['for-each-ref', 'refs/heads',
1958 '--format=%(refname:short)']).splitlines()
1959
1960 # Reverse issue lookup.
1961 issue_branch_map = {}
1962 for branch in branches:
1963 cl = Changelist(branchref=branch)
1964 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
1965 if not args:
1966 args = sorted(issue_branch_map.iterkeys())
1967 for issue in args:
1968 if not issue:
1969 continue
1970 print 'Branch for issue number %s: %s' % (
1971 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
1972 else:
1973 cl = Changelist()
1974 if len(args) > 0:
1975 try:
1976 issue = int(args[0])
1977 except ValueError:
1978 DieWithError('Pass a number to set the issue or none to list it.\n'
1979 'Maybe you want to run git cl status?')
1980 cl.SetIssue(issue)
1981 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001982 return 0
1983
1984
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001985def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001986 """Shows or posts review comments for any changelist."""
1987 parser.add_option('-a', '--add-comment', dest='comment',
1988 help='comment to add to an issue')
1989 parser.add_option('-i', dest='issue',
1990 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00001991 parser.add_option('-j', '--json-file',
1992 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001993 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001994 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001995 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001996
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001997 issue = None
1998 if options.issue:
1999 try:
2000 issue = int(options.issue)
2001 except ValueError:
2002 DieWithError('A review issue id is expected to be a number')
2003
dnj@chromium.orgf585b1f2016-03-28 22:25:16 +00002004 cl = Changelist(issue=issue, auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002005
2006 if options.comment:
2007 cl.AddComment(options.comment)
2008 return 0
2009
2010 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00002011 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00002012 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00002013 summary.append({
2014 'date': message['date'],
2015 'lgtm': False,
2016 'message': message['text'],
2017 'not_lgtm': False,
2018 'sender': message['sender'],
2019 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002020 if message['disapproval']:
2021 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00002022 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002023 elif message['approval']:
2024 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00002025 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002026 elif message['sender'] == data['owner_email']:
2027 color = Fore.MAGENTA
2028 else:
2029 color = Fore.BLUE
2030 print '\n%s%s %s%s' % (
2031 color, message['date'].split('.', 1)[0], message['sender'],
2032 Fore.RESET)
2033 if message['text'].strip():
2034 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00002035 if options.json_file:
2036 with open(options.json_file, 'wb') as f:
2037 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002038 return 0
2039
2040
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002041def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002042 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00002043 parser.add_option('-d', '--display', action='store_true',
2044 help='Display the description instead of opening an editor')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002045 auth.add_auth_options(parser)
2046 options, _ = parser.parse_args(args)
2047 auth_config = auth.extract_auth_config_from_options(options)
2048 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002049 if not cl.GetIssue():
2050 DieWithError('This branch has no associated changelist.')
2051 description = ChangeDescription(cl.GetDescription())
smut@google.com34fb6b12015-07-13 20:03:26 +00002052 if options.display:
2053 print description.description
2054 return 0
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002055 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00002056 if cl.GetDescription() != description.description:
2057 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002058 return 0
2059
2060
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002061def CreateDescriptionFromLog(args):
2062 """Pulls out the commit log to use as a base for the CL description."""
2063 log_args = []
2064 if len(args) == 1 and not args[0].endswith('.'):
2065 log_args = [args[0] + '..']
2066 elif len(args) == 1 and args[0].endswith('...'):
2067 log_args = [args[0][:-1]]
2068 elif len(args) == 2:
2069 log_args = [args[0] + '..' + args[1]]
2070 else:
2071 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00002072 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002073
2074
thestig@chromium.org44202a22014-03-11 19:22:18 +00002075def CMDlint(parser, args):
2076 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002077 parser.add_option('--filter', action='append', metavar='-x,+y',
2078 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002079 auth.add_auth_options(parser)
2080 options, args = parser.parse_args(args)
2081 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002082
2083 # Access to a protected member _XX of a client class
2084 # pylint: disable=W0212
2085 try:
2086 import cpplint
2087 import cpplint_chromium
2088 except ImportError:
2089 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
2090 return 1
2091
2092 # Change the current working directory before calling lint so that it
2093 # shows the correct base.
2094 previous_cwd = os.getcwd()
2095 os.chdir(settings.GetRoot())
2096 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002097 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002098 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
2099 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00002100 if not files:
2101 print "Cannot lint an empty CL"
2102 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00002103
2104 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002105 command = args + files
2106 if options.filter:
2107 command = ['--filter=' + ','.join(options.filter)] + command
2108 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002109
2110 white_regex = re.compile(settings.GetLintRegex())
2111 black_regex = re.compile(settings.GetLintIgnoreRegex())
2112 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
2113 for filename in filenames:
2114 if white_regex.match(filename):
2115 if black_regex.match(filename):
2116 print "Ignoring file %s" % filename
2117 else:
2118 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
2119 extra_check_functions)
2120 else:
2121 print "Skipping file %s" % filename
2122 finally:
2123 os.chdir(previous_cwd)
2124 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
2125 if cpplint._cpplint_state.error_count != 0:
2126 return 1
2127 return 0
2128
2129
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002130def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002131 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002132 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002133 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002134 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00002135 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002136 auth.add_auth_options(parser)
2137 options, args = parser.parse_args(args)
2138 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002139
sbc@chromium.org71437c02015-04-09 19:29:40 +00002140 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00002141 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002142 return 1
2143
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002144 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002145 if args:
2146 base_branch = args[0]
2147 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002148 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002149 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002150
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002151 cl.RunHook(
2152 committing=not options.upload,
2153 may_prompt=False,
2154 verbose=options.verbose,
2155 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00002156 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002157
2158
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002159def AddChangeIdToCommitMessage(options, args):
2160 """Re-commits using the current message, assumes the commit hook is in
2161 place.
2162 """
2163 log_desc = options.message or CreateDescriptionFromLog(args)
2164 git_command = ['commit', '--amend', '-m', log_desc]
2165 RunGit(git_command)
2166 new_log_desc = CreateDescriptionFromLog(args)
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00002167 if git_footers.get_footer_change_id(new_log_desc):
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002168 print 'git-cl: Added Change-Id to commit message.'
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002169 return new_log_desc
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002170 else:
2171 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
2172
2173
tandrii@chromium.org65874e12016-03-04 12:03:02 +00002174def GenerateGerritChangeId(message):
2175 """Returns Ixxxxxx...xxx change id.
2176
2177 Works the same way as
2178 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
2179 but can be called on demand on all platforms.
2180
2181 The basic idea is to generate git hash of a state of the tree, original commit
2182 message, author/committer info and timestamps.
2183 """
2184 lines = []
2185 tree_hash = RunGitSilent(['write-tree'])
2186 lines.append('tree %s' % tree_hash.strip())
2187 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
2188 if code == 0:
2189 lines.append('parent %s' % parent.strip())
2190 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
2191 lines.append('author %s' % author.strip())
2192 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
2193 lines.append('committer %s' % committer.strip())
2194 lines.append('')
2195 # Note: Gerrit's commit-hook actually cleans message of some lines and
2196 # whitespace. This code is not doing this, but it clearly won't decrease
2197 # entropy.
2198 lines.append(message)
2199 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
2200 stdin='\n'.join(lines))
2201 return 'I%s' % change_hash.strip()
2202
2203
piman@chromium.org336f9122014-09-04 02:16:55 +00002204def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002205 """upload the current branch to gerrit."""
tandrii@chromium.org95ffb612016-03-28 15:44:07 +00002206 # We assume the remote called "origin" is the one we want.
2207 # It is probably not worthwhile to support different workflows.
2208 gerrit_remote = 'origin'
2209
luqui@chromium.org609f3952015-05-04 22:47:04 +00002210 remote, remote_branch = cl.GetRemoteBranch()
2211 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2212 pending_prefix='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002213
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002214 change_desc = ChangeDescription(
2215 options.message or CreateDescriptionFromLog(args))
2216 if not change_desc.description:
andybons@chromium.org962f9462016-02-03 20:00:42 +00002217 print "\nDescription is empty. Aborting..."
2218 return 1
2219
2220 if options.title:
2221 print "\nPatch titles (-t) are not supported in Gerrit. Aborting..."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002222 return 1
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002223
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002224 if options.squash:
2225 # Try to get the message from a previous upload.
2226 shadow_branch = 'refs/heads/git_cl_uploads/' + cl.GetBranch()
bauerb@chromium.org13502e02016-02-18 10:18:29 +00002227 message = RunGitSilent(['show', '--format=%B', '-s', shadow_branch])
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002228 if not message:
2229 if not options.force:
2230 change_desc.prompt()
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002231 if not change_desc.description:
2232 print "Description is empty; aborting."
2233 return 1
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002234 message = change_desc.description
tandrii@chromium.org57d86542016-03-04 16:11:32 +00002235 change_ids = git_footers.get_footer_change_id(message)
2236 if len(change_ids) > 1:
2237 DieWithError('too many Change-Id footers in %s branch' % shadow_branch)
2238 if not change_ids:
2239 message = git_footers.add_footer_change_id(
2240 message, GenerateGerritChangeId(message))
2241 change_desc.set_description(message)
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002242 change_ids = git_footers.get_footer_change_id(message)
2243 assert len(change_ids) == 1
2244
2245 change_id = change_ids[0]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002246
2247 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2248 if remote is '.':
2249 # If our upstream branch is local, we base our squashed commit on its
2250 # squashed version.
2251 parent = ('refs/heads/git_cl_uploads/' +
2252 scm.GIT.ShortBranchName(upstream_branch))
2253
2254 # Verify that the upstream branch has been uploaded too, otherwise Gerrit
2255 # will create additional CLs when uploading.
2256 if (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2257 RunGitSilent(['rev-parse', parent + ':'])):
2258 print 'Upload upstream branch ' + upstream_branch + ' first.'
2259 return 1
2260 else:
2261 parent = cl.GetCommonAncestorWithUpstream()
2262
2263 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2264 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2265 '-m', message]).strip()
2266 else:
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00002267 if not git_footers.get_footer_change_id(change_desc.description):
tandrii@chromium.org10625002016-03-04 20:03:47 +00002268 DownloadGerritHook(False)
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002269 change_desc.set_description(AddChangeIdToCommitMessage(options, args))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002270 ref_to_push = 'HEAD'
tandrii@chromium.org95ffb612016-03-28 15:44:07 +00002271 parent = '%s/%s' % (gerrit_remote, branch)
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002272 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002273
2274 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2275 ref_to_push)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002276 if len(commits) > 1:
2277 print('WARNING: This will upload %d commits. Run the following command '
2278 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002279 print('git log %s..%s' % (parent, ref_to_push))
2280 print('You can also use `git squash-branch` to squash these into a single '
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002281 'commit.')
2282 ask_for_data('About to upload; enter to confirm.')
2283
piman@chromium.org336f9122014-09-04 02:16:55 +00002284 if options.reviewers or options.tbr_owners:
2285 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002286
ukai@chromium.orge8077812012-02-03 03:41:46 +00002287 receive_options = []
2288 cc = cl.GetCCList().split(',')
2289 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002290 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002291 cc = filter(None, cc)
2292 if cc:
2293 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002294 if change_desc.get_reviewers():
2295 receive_options.extend(
2296 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002297
ukai@chromium.orge8077812012-02-03 03:41:46 +00002298 git_command = ['push']
2299 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00002300 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00002301 ' '.join(receive_options))
tandrii@chromium.org95ffb612016-03-28 15:44:07 +00002302 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002303 push_stdout = gclient_utils.CheckCallAndFilter(
2304 ['git'] + git_command,
2305 print_stdout=True,
2306 # Flush after every line: useful for seeing progress when running as
2307 # recipe.
2308 filter_fn=lambda _: sys.stdout.flush())
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002309
2310 if options.squash:
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002311 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2312 change_numbers = [m.group(1)
2313 for m in map(regex.match, push_stdout.splitlines())
2314 if m]
2315 if len(change_numbers) != 1:
2316 DieWithError(
2317 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2318 'Change-Id: %s') % (len(change_numbers), change_id))
2319 cl.SetIssue(change_numbers[0])
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002320 head = RunGit(['rev-parse', 'HEAD']).strip()
2321 RunGit(['update-ref', '-m', 'Uploaded ' + head, shadow_branch, ref_to_push])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002322 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002323
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002324
wittman@chromium.org455dc922015-01-26 20:15:50 +00002325def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
2326 """Computes the remote branch ref to use for the CL.
2327
2328 Args:
2329 remote (str): The git remote for the CL.
2330 remote_branch (str): The git remote branch for the CL.
2331 target_branch (str): The target branch specified by the user.
2332 pending_prefix (str): The pending prefix from the settings.
2333 """
2334 if not (remote and remote_branch):
2335 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002336
wittman@chromium.org455dc922015-01-26 20:15:50 +00002337 if target_branch:
2338 # Cannonicalize branch references to the equivalent local full symbolic
2339 # refs, which are then translated into the remote full symbolic refs
2340 # below.
2341 if '/' not in target_branch:
2342 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
2343 else:
2344 prefix_replacements = (
2345 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
2346 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
2347 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
2348 )
2349 match = None
2350 for regex, replacement in prefix_replacements:
2351 match = re.search(regex, target_branch)
2352 if match:
2353 remote_branch = target_branch.replace(match.group(0), replacement)
2354 break
2355 if not match:
2356 # This is a branch path but not one we recognize; use as-is.
2357 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00002358 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
2359 # Handle the refs that need to land in different refs.
2360 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002361
wittman@chromium.org455dc922015-01-26 20:15:50 +00002362 # Create the true path to the remote branch.
2363 # Does the following translation:
2364 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
2365 # * refs/remotes/origin/master -> refs/heads/master
2366 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
2367 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
2368 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
2369 elif remote_branch.startswith('refs/remotes/%s/' % remote):
2370 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
2371 'refs/heads/')
2372 elif remote_branch.startswith('refs/remotes/branch-heads'):
2373 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
2374 # If a pending prefix exists then replace refs/ with it.
2375 if pending_prefix:
2376 remote_branch = remote_branch.replace('refs/', pending_prefix)
2377 return remote_branch
2378
2379
piman@chromium.org336f9122014-09-04 02:16:55 +00002380def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002381 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002382 upload_args = ['--assume_yes'] # Don't ask about untracked files.
dnj@chromium.orgf585b1f2016-03-28 22:25:16 +00002383 upload_args.extend(['--server', cl.GetRietveldServer()])
2384 upload_args.extend(auth.auth_config_to_command_options(cl.auth_config))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002385 if options.emulate_svn_auto_props:
2386 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002387
2388 change_desc = None
2389
pgervais@chromium.org91141372014-01-09 23:27:20 +00002390 if options.email is not None:
2391 upload_args.extend(['--email', options.email])
2392
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002393 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002394 if options.title:
2395 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00002396 if options.message:
2397 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00002398 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002399 print ("This branch is associated with issue %s. "
2400 "Adding patch to that issue." % cl.GetIssue())
2401 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002402 if options.title:
2403 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00002404 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002405 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00002406 if options.reviewers or options.tbr_owners:
2407 change_desc.update_reviewers(options.reviewers,
2408 options.tbr_owners,
2409 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002410 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002411 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002412
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002413 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002414 print "Description is empty; aborting."
2415 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002416
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002417 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002418 if change_desc.get_reviewers():
2419 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00002420 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002421 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00002422 DieWithError("Must specify reviewers to send email.")
2423 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00002424
2425 # We check this before applying rietveld.private assuming that in
2426 # rietveld.cc only addresses which we can send private CLs to are listed
2427 # if rietveld.private is set, and so we should ignore rietveld.cc only when
2428 # --private is specified explicitly on the command line.
2429 if options.private:
2430 logging.warn('rietveld.cc is ignored since private flag is specified. '
2431 'You need to review and add them manually if necessary.')
2432 cc = cl.GetCCListWithoutDefault()
2433 else:
2434 cc = cl.GetCCList()
2435 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00002436 if cc:
2437 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002438
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002439 if options.private or settings.GetDefaultPrivateFlag() == "True":
2440 upload_args.append('--private')
2441
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002442 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00002443 if not options.find_copies:
2444 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002445
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002446 # Include the upstream repo's URL in the change -- this is useful for
2447 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002448 remote_url = cl.GetGitBaseUrlFromConfig()
2449 if not remote_url:
2450 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002451 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002452 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00002453 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
2454 remote_url = (cl.GetRemoteUrl() + '@'
2455 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002456 if remote_url:
2457 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00002458 remote, remote_branch = cl.GetRemoteBranch()
wittman@chromium.org455dc922015-01-26 20:15:50 +00002459 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2460 settings.GetPendingRefPrefix())
2461 if target_ref:
2462 upload_args.extend(['--target_ref', target_ref])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002463
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002464 # Look for dependent patchsets. See crbug.com/480453 for more details.
2465 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2466 upstream_branch = ShortBranchName(upstream_branch)
2467 if remote is '.':
2468 # A local branch is being tracked.
2469 local_branch = ShortBranchName(upstream_branch)
rmistry@google.com78948ed2015-07-08 23:09:57 +00002470 if settings.GetIsSkipDependencyUpload(local_branch):
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002471 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002472 print ('Skipping dependency patchset upload because git config '
2473 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002474 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002475 else:
2476 auth_config = auth.extract_auth_config_from_options(options)
2477 branch_cl = Changelist(branchref=local_branch, auth_config=auth_config)
2478 branch_cl_issue_url = branch_cl.GetIssueURL()
2479 branch_cl_issue = branch_cl.GetIssue()
2480 branch_cl_patchset = branch_cl.GetPatchset()
2481 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2482 upload_args.extend(
2483 ['--depends_on_patchset', '%s:%s' % (
2484 branch_cl_issue, branch_cl_patchset)])
2485 print
2486 print ('The current branch (%s) is tracking a local branch (%s) with '
2487 'an associated CL.') % (cl.GetBranch(), local_branch)
2488 print 'Adding %s/#ps%s as a dependency patchset.' % (
2489 branch_cl_issue_url, branch_cl_patchset)
2490 print
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002491
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002492 project = settings.GetProject()
2493 if project:
2494 upload_args.extend(['--project', project])
2495
rmistry@google.comef966222015-04-07 11:15:01 +00002496 if options.cq_dry_run:
2497 upload_args.extend(['--cq_dry_run'])
2498
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002499 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00002500 upload_args = ['upload'] + upload_args + args
2501 logging.info('upload.RealMain(%s)', upload_args)
2502 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00002503 issue = int(issue)
2504 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00002505 except KeyboardInterrupt:
2506 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002507 except:
2508 # If we got an exception after the user typed a description for their
2509 # change, back up the description before re-raising.
2510 if change_desc:
2511 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2512 print '\nGot exception while uploading -- saving description to %s\n' \
2513 % backup_path
2514 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002515 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002516 backup_file.close()
2517 raise
2518
2519 if not cl.GetIssue():
2520 cl.SetIssue(issue)
2521 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002522
2523 if options.use_commit_queue:
2524 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002525 return 0
2526
2527
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002528def cleanup_list(l):
2529 """Fixes a list so that comma separated items are put as individual items.
2530
2531 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
2532 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
2533 """
2534 items = sum((i.split(',') for i in l), [])
2535 stripped_items = (i.strip() for i in items)
2536 return sorted(filter(None, stripped_items))
2537
2538
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002539@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002540def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00002541 """Uploads the current changelist to codereview.
2542
2543 Can skip dependency patchset uploads for a branch by running:
2544 git config branch.branch_name.skip-deps-uploads True
2545 To unset run:
2546 git config --unset branch.branch_name.skip-deps-uploads
2547 Can also set the above globally by using the --global flag.
2548 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00002549 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2550 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002551 parser.add_option('--bypass-watchlists', action='store_true',
2552 dest='bypass_watchlists',
2553 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002554 parser.add_option('-f', action='store_true', dest='force',
2555 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002556 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00002557 parser.add_option('-t', dest='title',
2558 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002559 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002560 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002561 help='reviewer email addresses')
2562 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002563 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002564 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00002565 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00002566 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002567 parser.add_option('--emulate_svn_auto_props',
2568 '--emulate-svn-auto-props',
2569 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00002570 dest="emulate_svn_auto_props",
2571 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00002572 parser.add_option('-c', '--use-commit-queue', action='store_true',
2573 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002574 parser.add_option('--private', action='store_true',
2575 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00002576 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002577 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00002578 metavar='TARGET',
2579 help='Apply CL to remote ref TARGET. ' +
2580 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002581 parser.add_option('--squash', action='store_true',
2582 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002583 parser.add_option('--no-squash', action='store_true',
2584 help='Don\'t squash multiple commits into one ' +
2585 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002586 parser.add_option('--email', default=None,
2587 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00002588 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
2589 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00002590 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
2591 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00002592 help='Send the patchset to do a CQ dry run right after '
2593 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00002594 parser.add_option('--dependencies', action='store_true',
2595 help='Uploads CLs of all the local branches that depend on '
2596 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002597
rmistry@google.com2dd99862015-06-22 12:22:18 +00002598 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002599 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002600 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002601 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002602 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002603
sbc@chromium.org71437c02015-04-09 19:29:40 +00002604 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002605 return 1
2606
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002607 options.reviewers = cleanup_list(options.reviewers)
2608 options.cc = cleanup_list(options.cc)
2609
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002610 cl = Changelist(auth_config=auth_config)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002611 if args:
2612 # TODO(ukai): is it ok for gerrit case?
2613 base_branch = args[0]
2614 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00002615 if cl.GetBranch() is None:
2616 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
2617
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002618 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002619 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00002620 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00002621
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00002622 # Make sure authenticated to Rietveld before running expensive hooks. It is
2623 # a fast, best efforts check. Rietveld still can reject the authentication
2624 # during the actual upload.
2625 if not settings.GetIsGerrit() and auth_config.use_oauth2:
2626 authenticator = auth.get_authenticator_for_host(
dnj@chromium.orgf585b1f2016-03-28 22:25:16 +00002627 cl.GetRietveldServer(), auth_config)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00002628 if not authenticator.has_cached_credentials():
dnj@chromium.orgf585b1f2016-03-28 22:25:16 +00002629 raise auth.LoginRequiredError(cl.GetRietveldServer())
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00002630
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002631 # Apply watchlists on upload.
2632 change = cl.GetChange(base_branch, None)
2633 watchlist = watchlists.Watchlists(change.RepositoryRoot())
2634 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002635 if not options.bypass_watchlists:
2636 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002637
ukai@chromium.orge8077812012-02-03 03:41:46 +00002638 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00002639 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002640 # Set the reviewer list now so that presubmit checks can access it.
2641 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00002642 change_description.update_reviewers(options.reviewers,
2643 options.tbr_owners,
2644 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002645 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002646 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00002647 may_prompt=not options.force,
2648 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002649 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002650 if not hook_results.should_continue():
2651 return 1
2652 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002653 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002654
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002655 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002656 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002657 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00002658 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002659 print ('The last upload made from this repository was patchset #%d but '
2660 'the most recent patchset on the server is #%d.'
2661 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00002662 print ('Uploading will still work, but if you\'ve uploaded to this issue '
2663 'from another machine or branch the patch you\'re uploading now '
2664 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002665 ask_for_data('About to upload; enter to confirm.')
2666
iannucci@chromium.org79540052012-10-19 23:15:26 +00002667 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002668 if settings.GetIsGerrit():
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002669 if options.squash and options.no_squash:
2670 DieWithError('Can only use one of --squash or --no-squash')
2671
2672 options.squash = ((settings.GetSquashGerritUploads() or options.squash) and
2673 not options.no_squash)
2674
tandrii@chromium.org1e67bb72016-02-11 12:15:49 +00002675 ret = GerritUpload(options, args, cl, change)
2676 else:
2677 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002678 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00002679 git_set_branch_value('last-upload-hash',
2680 RunGit(['rev-parse', 'HEAD']).strip())
rmistry@google.com5626a922015-02-26 14:03:30 +00002681 # Run post upload hooks, if specified.
2682 if settings.GetRunPostUploadHook():
2683 presubmit_support.DoPostUploadExecuter(
2684 change,
2685 cl,
2686 settings.GetRoot(),
2687 options.verbose,
2688 sys.stdout)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002689
rmistry@google.com2dd99862015-06-22 12:22:18 +00002690 # Upload all dependencies if specified.
2691 if options.dependencies:
2692 print
2693 print '--dependencies has been specified.'
2694 print 'All dependent local branches will be re-uploaded.'
2695 print
2696 # Remove the dependencies flag from args so that we do not end up in a
2697 # loop.
2698 orig_args.remove('--dependencies')
2699 upload_branch_deps(cl, orig_args)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002700 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00002701
2702
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002703def IsSubmoduleMergeCommit(ref):
2704 # When submodules are added to the repo, we expect there to be a single
2705 # non-git-svn merge commit at remote HEAD with a signature comment.
2706 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00002707 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002708 return RunGit(cmd) != ''
2709
2710
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002711def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002712 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002713
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002714 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002715 Updates changelog with metadata (e.g. pointer to review).
2716 Pushes/dcommits the code upstream.
2717 Updates review and closes.
2718 """
2719 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2720 help='bypass upload presubmit hook')
2721 parser.add_option('-m', dest='message',
2722 help="override review description")
2723 parser.add_option('-f', action='store_true', dest='force',
2724 help="force yes to questions (don't prompt)")
2725 parser.add_option('-c', dest='contributor',
2726 help="external contributor for patch (appended to " +
2727 "description and used as author for git). Should be " +
2728 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002729 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002730 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002731 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002732 auth_config = auth.extract_auth_config_from_options(options)
2733
2734 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002735
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002736 current = cl.GetBranch()
2737 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2738 if not settings.GetIsGitSvn() and remote == '.':
2739 print
2740 print 'Attempting to push branch %r into another local branch!' % current
2741 print
2742 print 'Either reparent this branch on top of origin/master:'
2743 print ' git reparent-branch --root'
2744 print
2745 print 'OR run `git rebase-update` if you think the parent branch is already'
2746 print 'committed.'
2747 print
2748 print ' Current parent: %r' % upstream_branch
2749 return 1
2750
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002751 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002752 # Default to merging against our best guess of the upstream branch.
2753 args = [cl.GetUpstreamBranch()]
2754
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002755 if options.contributor:
2756 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
2757 print "Please provide contibutor as 'First Last <email@example.com>'"
2758 return 1
2759
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002760 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002761 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002762
sbc@chromium.org71437c02015-04-09 19:29:40 +00002763 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002764 return 1
2765
2766 # This rev-list syntax means "show all commits not in my branch that
2767 # are in base_branch".
2768 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
2769 base_branch]).splitlines()
2770 if upstream_commits:
2771 print ('Base branch "%s" has %d commits '
2772 'not in this branch.' % (base_branch, len(upstream_commits)))
2773 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
2774 return 1
2775
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002776 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002777 svn_head = None
2778 if cmd == 'dcommit' or base_has_submodules:
2779 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
2780 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002781
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002782 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002783 # If the base_head is a submodule merge commit, the first parent of the
2784 # base_head should be a git-svn commit, which is what we're interested in.
2785 base_svn_head = base_branch
2786 if base_has_submodules:
2787 base_svn_head += '^1'
2788
2789 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002790 if extra_commits:
2791 print ('This branch has %d additional commits not upstreamed yet.'
2792 % len(extra_commits.splitlines()))
2793 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
2794 'before attempting to %s.' % (base_branch, cmd))
2795 return 1
2796
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002797 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002798 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002799 author = None
2800 if options.contributor:
2801 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002802 hook_results = cl.RunHook(
2803 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002804 may_prompt=not options.force,
2805 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002806 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002807 if not hook_results.should_continue():
2808 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002809
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002810 # Check the tree status if the tree status URL is set.
2811 status = GetTreeStatus()
2812 if 'closed' == status:
2813 print('The tree is closed. Please wait for it to reopen. Use '
2814 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2815 return 1
2816 elif 'unknown' == status:
2817 print('Unable to determine tree status. Please verify manually and '
2818 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2819 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002820
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002821 change_desc = ChangeDescription(options.message)
2822 if not change_desc.description and cl.GetIssue():
2823 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002824
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002825 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00002826 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002827 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00002828 else:
2829 print 'No description set.'
2830 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
2831 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002832
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002833 # Keep a separate copy for the commit message, because the commit message
2834 # contains the link to the Rietveld issue, while the Rietveld message contains
2835 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002836 # Keep a separate copy for the commit message.
2837 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00002838 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002839
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002840 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00002841 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00002842 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00002843 # after it. Add a period on a new line to circumvent this. Also add a space
2844 # before the period to make sure that Gitiles continues to correctly resolve
2845 # the URL.
2846 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002847 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002848 commit_desc.append_footer('Patch from %s.' % options.contributor)
2849
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00002850 print('Description:')
2851 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002852
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002853 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002854 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00002855 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002856
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002857 # We want to squash all this branch's commits into one commit with the proper
2858 # description. We do this by doing a "reset --soft" to the base branch (which
2859 # keeps the working copy the same), then dcommitting that. If origin/master
2860 # has a submodule merge commit, we'll also need to cherry-pick the squashed
2861 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002862 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002863 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
2864 # Delete the branches if they exist.
2865 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
2866 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
2867 result = RunGitWithCode(showref_cmd)
2868 if result[0] == 0:
2869 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002870
2871 # We might be in a directory that's present in this branch but not in the
2872 # trunk. Move up to the top of the tree so that git commands that expect a
2873 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002874 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002875 if rel_base_path:
2876 os.chdir(rel_base_path)
2877
2878 # Stuff our change into the merge branch.
2879 # We wrap in a try...finally block so if anything goes wrong,
2880 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002881 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002882 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002883 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002884 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002885 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00002886 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002887 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002888 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002889 RunGit(
2890 [
2891 'commit', '--author', options.contributor,
2892 '-m', commit_desc.description,
2893 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002894 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002895 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002896 if base_has_submodules:
2897 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
2898 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
2899 RunGit(['checkout', CHERRY_PICK_BRANCH])
2900 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002901 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002902 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00002903 mirror = settings.GetGitMirror(remote)
2904 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002905 pending_prefix = settings.GetPendingRefPrefix()
2906 if not pending_prefix or branch.startswith(pending_prefix):
2907 # If not using refs/pending/heads/* at all, or target ref is already set
2908 # to pending, then push to the target ref directly.
2909 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00002910 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002911 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002912 else:
2913 # Cherry-pick the change on top of pending ref and then push it.
2914 assert branch.startswith('refs/'), branch
2915 assert pending_prefix[-1] == '/', pending_prefix
2916 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00002917 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002918 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002919 if retcode == 0:
2920 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002921 else:
2922 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002923 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002924 'svn', 'dcommit',
2925 '-C%s' % options.similarity,
2926 '--no-rebase', '--rmdir',
2927 ]
2928 if settings.GetForceHttpsCommitUrl():
2929 # Allow forcing https commit URLs for some projects that don't allow
2930 # committing to http URLs (like Google Code).
2931 remote_url = cl.GetGitSvnRemoteUrl()
2932 if urlparse.urlparse(remote_url).scheme == 'http':
2933 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002934 cmd_args.append('--commit-url=%s' % remote_url)
2935 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002936 if 'Committed r' in output:
2937 revision = re.match(
2938 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2939 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002940 finally:
2941 # And then swap back to the original branch and clean up.
2942 RunGit(['checkout', '-q', cl.GetBranch()])
2943 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002944 if base_has_submodules:
2945 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002946
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002947 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002948 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002949 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002950
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002951 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002952 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002953 try:
2954 revision = WaitForRealCommit(remote, revision, base_branch, branch)
2955 # We set pushed_to_pending to False, since it made it all the way to the
2956 # real ref.
2957 pushed_to_pending = False
2958 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002959 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002960
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002961 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002962 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002963 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002964 if not to_pending:
2965 if viewvc_url and revision:
2966 change_desc.append_footer(
2967 'Committed: %s%s' % (viewvc_url, revision))
2968 elif revision:
2969 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002970 print ('Closing issue '
2971 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002972 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002973 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002974 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002975 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00002976 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00002977 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002978 if options.bypass_hooks:
2979 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2980 else:
2981 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002982 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002983 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002984
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002985 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002986 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
2987 print 'The commit is in the pending queue (%s).' % pending_ref
2988 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00002989 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002990 'footer.' % branch)
2991
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002992 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2993 if os.path.isfile(hook):
2994 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002995
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002996 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002997
2998
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002999def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
3000 print
3001 print 'Waiting for commit to be landed on %s...' % real_ref
3002 print '(If you are impatient, you may Ctrl-C once without harm)'
3003 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
3004 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003005 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003006
3007 loop = 0
3008 while True:
3009 sys.stdout.write('fetching (%d)... \r' % loop)
3010 sys.stdout.flush()
3011 loop += 1
3012
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003013 if mirror:
3014 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003015 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
3016 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
3017 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
3018 for commit in commits.splitlines():
3019 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
3020 print 'Found commit on %s' % real_ref
3021 return commit
3022
3023 current_rev = to_rev
3024
3025
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003026def PushToGitPending(remote, pending_ref, upstream_ref):
3027 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
3028
3029 Returns:
3030 (retcode of last operation, output log of last operation).
3031 """
3032 assert pending_ref.startswith('refs/'), pending_ref
3033 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
3034 cherry = RunGit(['rev-parse', 'HEAD']).strip()
3035 code = 0
3036 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003037 max_attempts = 3
3038 attempts_left = max_attempts
3039 while attempts_left:
3040 if attempts_left != max_attempts:
3041 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
3042 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003043
3044 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003045 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003046 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003047 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003048 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003049 print 'Fetch failed with exit code %d.' % code
3050 if out.strip():
3051 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003052 continue
3053
3054 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003055 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003056 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003057 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003058 if code:
3059 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003060 'Your patch doesn\'t apply cleanly to ref \'%s\', '
3061 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003062 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
3063 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003064 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003065 return code, out
3066
3067 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003068 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003069 code, out = RunGitWithCode(
3070 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
3071 if code == 0:
3072 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003073 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003074 return code, out
3075
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003076 print 'Push failed with exit code %d.' % code
3077 if out.strip():
3078 print out.strip()
3079 if IsFatalPushFailure(out):
3080 print (
3081 'Fatal push error. Make sure your .netrc credentials and git '
3082 'user.email are correct and you have push access to the repo.')
3083 return code, out
3084
3085 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003086 return code, out
3087
3088
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003089def IsFatalPushFailure(push_stdout):
3090 """True if retrying push won't help."""
3091 return '(prohibited by Gerrit)' in push_stdout
3092
3093
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003094@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003095def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003096 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003097 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003098 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003099 # If it looks like previous commits were mirrored with git-svn.
3100 message = """This repository appears to be a git-svn mirror, but no
3101upstream SVN master is set. You probably need to run 'git auto-svn' once."""
3102 else:
3103 message = """This doesn't appear to be an SVN repository.
3104If your project has a true, writeable git repository, you probably want to run
3105'git cl land' instead.
3106If your project has a git mirror of an upstream SVN master, you probably need
3107to run 'git svn init'.
3108
3109Using the wrong command might cause your commit to appear to succeed, and the
3110review to be closed, without actually landing upstream. If you choose to
3111proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00003112 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00003113 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003114 return SendUpstream(parser, args, 'dcommit')
3115
3116
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003117@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003118def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003119 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003120 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003121 print('This appears to be an SVN repository.')
3122 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003123 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00003124 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003125 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003126
3127
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003128def ParseIssueNum(arg):
3129 """Parses the issue number from args if present otherwise returns None."""
3130 if re.match(r'\d+', arg):
3131 return arg
3132 if arg.startswith('http'):
3133 return re.sub(r'.*/(\d+)/?', r'\1', arg)
3134 return None
3135
3136
3137@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003138def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00003139 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003140 parser.add_option('-b', dest='newbranch',
3141 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003142 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003143 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003144 parser.add_option('-d', '--directory', action='store', metavar='DIR',
3145 help='Change to the directory DIR immediately, '
3146 'before doing anything else.')
3147 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003148 help='failed patches spew .rej files rather than '
3149 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003150 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
3151 help="don't commit after patch applies")
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003152
3153 group = optparse.OptionGroup(parser,
3154 """Options for continuing work on the current issue uploaded
3155from a different clone (e.g. different machine). Must be used independently from
3156the other options. No issue number should be specified, and the branch must have
3157an issue number associated with it""")
3158 group.add_option('--reapply', action='store_true',
3159 dest='reapply',
3160 help="""Reset the branch and reapply the issue.
3161CAUTION: This will undo any local changes in this branch""")
3162
3163 group.add_option('--pull', action='store_true', dest='pull',
3164 help="Performs a pull before reapplying.")
3165 parser.add_option_group(group)
3166
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003167 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003168 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003169 auth_config = auth.extract_auth_config_from_options(options)
3170
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003171 issue_arg = None
3172 if options.reapply :
3173 if len(args) > 0:
3174 parser.error("--reapply implies no additional arguments.")
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003175
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003176 cl = Changelist()
3177 issue_arg = cl.GetIssue()
3178 upstream = cl.GetUpstreamBranch()
3179 if upstream == None:
3180 parser.error("No upstream branch specified. Cannot reset branch")
3181
3182 RunGit(['reset', '--hard', upstream])
3183 if options.pull:
3184 RunGit(['pull'])
3185 else:
3186 if len(args) != 1:
3187 parser.error("Must specify issue number")
3188
3189 issue_arg = ParseIssueNum(args[0])
3190
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003191 # The patch URL works because ParseIssueNum won't do any substitution
3192 # as the re.sub pattern fails to match and just returns it.
3193 if issue_arg == None:
3194 parser.print_help()
3195 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003196
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003197 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003198 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003199 return 1
3200
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003201 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00003202 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003203
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003204 if options.newbranch:
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003205 if options.reapply:
3206 parser.error("--reapply excludes any option other than --pull")
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003207 if options.force:
3208 RunGit(['branch', '-D', options.newbranch],
3209 stderr=subprocess2.PIPE, error_ok=True)
3210 RunGit(['checkout', '-b', options.newbranch,
3211 Changelist().GetUpstreamBranch()])
3212
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003213 return PatchIssue(issue_arg, options.reject, options.nocommit,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003214 options.directory, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003215
3216
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003217def PatchIssue(issue_arg, reject, nocommit, directory, auth_config):
wychen@chromium.orga872e752015-04-28 23:42:18 +00003218 # PatchIssue should never be called with a dirty tree. It is up to the
3219 # caller to check this, but just in case we assert here since the
3220 # consequences of the caller not checking this could be dire.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003221 assert(not git_common.is_dirty_git_tree('apply'))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003222
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003223 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003224 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00003225 issue = int(issue_arg)
dnj@chromium.orgf585b1f2016-03-28 22:25:16 +00003226 cl = Changelist(issue=issue, auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003227 patchset = cl.GetMostRecentPatchset()
dnj@chromium.orgf585b1f2016-03-28 22:25:16 +00003228 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003229 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00003230 # Assume it's a URL to the patch. Default to https.
3231 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
kjellander@chromium.org44424542015-06-02 18:35:29 +00003232 match = re.match(r'(.*?)/download/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00003233 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003234 DieWithError('Must pass an issue ID or full URL for '
3235 '\'Download raw patch set\'')
kjellander@chromium.org44424542015-06-02 18:35:29 +00003236 issue = int(match.group(2))
dnj@chromium.orgf585b1f2016-03-28 22:25:16 +00003237 cl = Changelist(issue=issue, auth_config=auth_config)
3238 cl.rietveld_server = match.group(1)
kjellander@chromium.org44424542015-06-02 18:35:29 +00003239 patchset = int(match.group(3))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00003240 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003241
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003242 # Switch up to the top-level directory, if necessary, in preparation for
3243 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003244 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003245 if top:
3246 os.chdir(top)
3247
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003248 # Git patches have a/ at the beginning of source paths. We strip that out
3249 # with a sed script rather than the -p flag to patch so we can feed either
3250 # Git or svn-style patches into the same apply command.
3251 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003252 try:
3253 patch_data = subprocess2.check_output(
3254 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
3255 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003256 DieWithError('Git patch mungling failed.')
3257 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003258
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003259 # We use "git apply" to apply the patch instead of "patch" so that we can
3260 # pick up file adds.
3261 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003262 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003263 if directory:
3264 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003265 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003266 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003267 elif IsGitVersionAtLeast('1.7.12'):
3268 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003269 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003270 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003271 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003272 except subprocess2.CalledProcessError:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003273 print 'Failed to apply the patch'
3274 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003275
3276 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003277 if not nocommit:
wychen@chromium.org5b3bebb2015-05-28 21:41:43 +00003278 RunGit(['commit', '-m', (cl.GetDescription() + '\n\n' +
3279 'patch from issue %(i)s at patchset '
carlosk@chromium.org71284d92014-11-14 18:12:50 +00003280 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
3281 % {'i': issue, 'p': patchset})])
dnj@chromium.orgf585b1f2016-03-28 22:25:16 +00003282 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003283 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00003284 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00003285 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003286 else:
3287 print "Patch applied to index."
3288 return 0
3289
3290
3291def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003292 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003293 # Provide a wrapper for git svn rebase to help avoid accidental
3294 # git svn dcommit.
3295 # It's the only command that doesn't use parser at all since we just defer
3296 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003297
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003298 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003299
3300
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003301def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003302 """Fetches the tree status and returns either 'open', 'closed',
3303 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003304 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003305 if url:
3306 status = urllib2.urlopen(url).read().lower()
3307 if status.find('closed') != -1 or status == '0':
3308 return 'closed'
3309 elif status.find('open') != -1 or status == '1':
3310 return 'open'
3311 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003312 return 'unset'
3313
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003314
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003315def GetTreeStatusReason():
3316 """Fetches the tree status from a json url and returns the message
3317 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00003318 url = settings.GetTreeStatusUrl()
3319 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003320 connection = urllib2.urlopen(json_url)
3321 status = json.loads(connection.read())
3322 connection.close()
3323 return status['message']
3324
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003325
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003326def GetBuilderMaster(bot_list):
3327 """For a given builder, fetch the master from AE if available."""
3328 map_url = 'https://builders-map.appspot.com/'
3329 try:
3330 master_map = json.load(urllib2.urlopen(map_url))
3331 except urllib2.URLError as e:
3332 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
3333 (map_url, e))
3334 except ValueError as e:
3335 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
3336 if not master_map:
3337 return None, 'Failed to build master map.'
3338
3339 result_master = ''
3340 for bot in bot_list:
3341 builder = bot.split(':', 1)[0]
3342 master_list = master_map.get(builder, [])
3343 if not master_list:
3344 return None, ('No matching master for builder %s.' % builder)
3345 elif len(master_list) > 1:
3346 return None, ('The builder name %s exists in multiple masters %s.' %
3347 (builder, master_list))
3348 else:
3349 cur_master = master_list[0]
3350 if not result_master:
3351 result_master = cur_master
3352 elif result_master != cur_master:
3353 return None, 'The builders do not belong to the same master.'
3354 return result_master, None
3355
3356
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003357def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003358 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003359 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003360 status = GetTreeStatus()
3361 if 'unset' == status:
3362 print 'You must configure your tree status URL by running "git cl config".'
3363 return 2
3364
3365 print "The tree is %s" % status
3366 print
3367 print GetTreeStatusReason()
3368 if status != 'open':
3369 return 1
3370 return 0
3371
3372
maruel@chromium.org15192402012-09-06 12:38:29 +00003373def CMDtry(parser, args):
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003374 """Triggers a try job through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00003375 group = optparse.OptionGroup(parser, "Try job options")
3376 group.add_option(
3377 "-b", "--bot", action="append",
3378 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
3379 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003380 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00003381 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003382 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00003383 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003384 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00003385 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003386 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003387 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003388 "-r", "--revision",
3389 help="Revision to use for the try job; default: the "
3390 "revision will be determined by the try server; see "
3391 "its waterfall for more info")
3392 group.add_option(
3393 "-c", "--clobber", action="store_true", default=False,
3394 help="Force a clobber before building; e.g. don't do an "
3395 "incremental build")
3396 group.add_option(
3397 "--project",
3398 help="Override which project to use. Projects are defined "
3399 "server-side to define what default bot set to use")
3400 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00003401 "-p", "--property", dest="properties", action="append", default=[],
3402 help="Specify generic properties in the form -p key1=value1 -p "
3403 "key2=value2 etc (buildbucket only). The value will be treated as "
3404 "json if decodable, or as string otherwise.")
3405 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003406 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003407 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003408 "--use-rietveld", action="store_true", default=False,
3409 help="Use Rietveld to trigger try jobs.")
3410 group.add_option(
3411 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3412 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00003413 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003414 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00003415 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003416 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00003417
machenbach@chromium.org45453142015-09-15 08:45:22 +00003418 if options.use_rietveld and options.properties:
3419 parser.error('Properties can only be specified with buildbucket')
3420
3421 # Make sure that all properties are prop=value pairs.
3422 bad_params = [x for x in options.properties if '=' not in x]
3423 if bad_params:
3424 parser.error('Got properties with missing "=": %s' % bad_params)
3425
maruel@chromium.org15192402012-09-06 12:38:29 +00003426 if args:
3427 parser.error('Unknown arguments: %s' % args)
3428
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003429 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00003430 if not cl.GetIssue():
3431 parser.error('Need to upload first')
3432
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003433 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00003434 if props.get('closed'):
3435 parser.error('Cannot send tryjobs for a closed CL')
3436
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003437 if props.get('private'):
3438 parser.error('Cannot use trybots with private issue')
3439
maruel@chromium.org15192402012-09-06 12:38:29 +00003440 if not options.name:
3441 options.name = cl.GetBranch()
3442
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003443 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003444 options.master, err_msg = GetBuilderMaster(options.bot)
3445 if err_msg:
3446 parser.error('Tryserver master cannot be found because: %s\n'
3447 'Please manually specify the tryserver master'
3448 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003449
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003450 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003451 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003452 if not options.bot:
3453 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00003454
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003455 # Get try masters from PRESUBMIT.py files.
3456 masters = presubmit_support.DoGetTryMasters(
3457 change,
3458 change.LocalPaths(),
3459 settings.GetRoot(),
3460 None,
3461 None,
3462 options.verbose,
3463 sys.stdout)
3464 if masters:
3465 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00003466
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003467 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
3468 options.bot = presubmit_support.DoGetTrySlaves(
3469 change,
3470 change.LocalPaths(),
3471 settings.GetRoot(),
3472 None,
3473 None,
3474 options.verbose,
3475 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00003476
3477 if not options.bot:
3478 # Get try masters from cq.cfg if any.
3479 # TODO(tandrii): some (but very few) projects store cq.cfg in different
3480 # location.
3481 cq_cfg = os.path.join(change.RepositoryRoot(),
3482 'infra', 'config', 'cq.cfg')
3483 if os.path.exists(cq_cfg):
3484 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00003485 cq_masters = commit_queue.get_master_builder_map(
3486 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00003487 for master, builders in cq_masters.iteritems():
3488 for builder in builders:
3489 # Skip presubmit builders, because these will fail without LGTM.
3490 if 'presubmit' not in builder.lower():
3491 masters.setdefault(master, {})[builder] = ['defaulttests']
3492 if masters:
3493 return masters
3494
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003495 if not options.bot:
3496 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00003497
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003498 builders_and_tests = {}
3499 # TODO(machenbach): The old style command-line options don't support
3500 # multiple try masters yet.
3501 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
3502 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
3503
3504 for bot in old_style:
3505 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003506 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003507 elif ',' in bot:
3508 parser.error('Specify one bot per --bot flag')
3509 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00003510 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003511
3512 for bot, tests in new_style:
3513 builders_and_tests.setdefault(bot, []).extend(tests)
3514
3515 # Return a master map with one master to be backwards compatible. The
3516 # master name defaults to an empty string, which will cause the master
3517 # not to be set on rietveld (deprecated).
3518 return {options.master: builders_and_tests}
3519
3520 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00003521
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003522 for builders in masters.itervalues():
3523 if any('triggered' in b for b in builders):
3524 print >> sys.stderr, (
3525 'ERROR You are trying to send a job to a triggered bot. This type of'
3526 ' bot requires an\ninitial job from a parent (usually a builder). '
3527 'Instead send your job to the parent.\n'
3528 'Bot list: %s' % builders)
3529 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00003530
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00003531 patchset = cl.GetMostRecentPatchset()
3532 if patchset and patchset != cl.GetPatchset():
3533 print(
3534 '\nWARNING Mismatch between local config and server. Did a previous '
3535 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3536 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003537 if options.luci:
3538 trigger_luci_job(cl, masters, options)
3539 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003540 try:
3541 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
3542 except BuildbucketResponseException as ex:
3543 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00003544 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003545 except Exception as e:
3546 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3547 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
3548 e, stacktrace)
3549 return 1
3550 else:
3551 try:
3552 cl.RpcServer().trigger_distributed_try_jobs(
3553 cl.GetIssue(), patchset, options.name, options.clobber,
3554 options.revision, masters)
3555 except urllib2.HTTPError as e:
3556 if e.code == 404:
3557 print('404 from rietveld; '
3558 'did you mean to use "git try" instead of "git cl try"?')
3559 return 1
3560 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003561
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003562 for (master, builders) in sorted(masters.iteritems()):
3563 if master:
3564 print 'Master: %s' % master
3565 length = max(len(builder) for builder in builders)
3566 for builder in sorted(builders):
3567 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00003568 return 0
3569
3570
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00003571def CMDtry_results(parser, args):
3572 group = optparse.OptionGroup(parser, "Try job results options")
3573 group.add_option(
3574 "-p", "--patchset", type=int, help="patchset number if not current.")
3575 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00003576 "--print-master", action='store_true', help="print master name as well.")
3577 group.add_option(
3578 "--color", action='store_true', default=sys.stdout.isatty(),
3579 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00003580 group.add_option(
3581 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3582 help="Host of buildbucket. The default host is %default.")
3583 parser.add_option_group(group)
3584 auth.add_auth_options(parser)
3585 options, args = parser.parse_args(args)
3586 if args:
3587 parser.error('Unrecognized args: %s' % ' '.join(args))
3588
3589 auth_config = auth.extract_auth_config_from_options(options)
3590 cl = Changelist(auth_config=auth_config)
3591 if not cl.GetIssue():
3592 parser.error('Need to upload first')
3593
3594 if not options.patchset:
3595 options.patchset = cl.GetMostRecentPatchset()
3596 if options.patchset and options.patchset != cl.GetPatchset():
3597 print(
3598 '\nWARNING Mismatch between local config and server. Did a previous '
3599 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3600 'Continuing using\npatchset %s.\n' % options.patchset)
3601 try:
3602 jobs = fetch_try_jobs(auth_config, cl, options)
3603 except BuildbucketResponseException as ex:
3604 print 'Buildbucket error: %s' % ex
3605 return 1
3606 except Exception as e:
3607 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3608 print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
3609 e, stacktrace)
3610 return 1
3611 print_tryjobs(options, jobs)
3612 return 0
3613
3614
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003615@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003616def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003617 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003618 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003619 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003620 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003621
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003622 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003623 if args:
3624 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003625 branch = cl.GetBranch()
3626 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003627 cl = Changelist()
3628 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003629
3630 # Clear configured merge-base, if there is one.
3631 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003632 else:
3633 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003634 return 0
3635
3636
thestig@chromium.org00858c82013-12-02 23:08:03 +00003637def CMDweb(parser, args):
3638 """Opens the current CL in the web browser."""
3639 _, args = parser.parse_args(args)
3640 if args:
3641 parser.error('Unrecognized args: %s' % ' '.join(args))
3642
3643 issue_url = Changelist().GetIssueURL()
3644 if not issue_url:
3645 print >> sys.stderr, 'ERROR No issue to open'
3646 return 1
3647
3648 webbrowser.open(issue_url)
3649 return 0
3650
3651
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003652def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003653 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003654 auth.add_auth_options(parser)
3655 options, args = parser.parse_args(args)
3656 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003657 if args:
3658 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003659 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003660 props = cl.GetIssueProperties()
3661 if props.get('private'):
3662 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003663 cl.SetFlag('commit', '1')
3664 return 0
3665
3666
groby@chromium.org411034a2013-02-26 15:12:01 +00003667def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003668 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003669 auth.add_auth_options(parser)
3670 options, args = parser.parse_args(args)
3671 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00003672 if args:
3673 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003674 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00003675 # Ensure there actually is an issue to close.
3676 cl.GetDescription()
3677 cl.CloseIssue()
3678 return 0
3679
3680
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003681def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003682 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003683 auth.add_auth_options(parser)
3684 options, args = parser.parse_args(args)
3685 auth_config = auth.extract_auth_config_from_options(options)
3686 if args:
3687 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003688
3689 # Uncommitted (staged and unstaged) changes will be destroyed by
3690 # "git reset --hard" if there are merging conflicts in PatchIssue().
3691 # Staged changes would be committed along with the patch from last
3692 # upload, hence counted toward the "last upload" side in the final
3693 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003694 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003695 return 1
3696
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003697 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003698 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003699 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003700 if not issue:
3701 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003702 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003703 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003704
3705 # Create a new branch based on the merge-base
3706 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
3707 try:
3708 # Patch in the latest changes from rietveld.
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003709 rtn = PatchIssue(issue, False, False, None, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003710 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003711 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003712 return rtn
3713
wychen@chromium.org06928532015-02-03 02:11:29 +00003714 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003715 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00003716 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003717 finally:
3718 RunGit(['checkout', '-q', branch])
3719 RunGit(['branch', '-D', TMP_BRANCH])
3720
3721 return 0
3722
3723
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003724def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003725 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003726 parser.add_option(
3727 '--no-color',
3728 action='store_true',
3729 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003730 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003731 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003732 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003733
3734 author = RunGit(['config', 'user.email']).strip() or None
3735
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003736 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003737
3738 if args:
3739 if len(args) > 1:
3740 parser.error('Unknown args')
3741 base_branch = args[0]
3742 else:
3743 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003744 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003745
3746 change = cl.GetChange(base_branch, None)
3747 return owners_finder.OwnersFinder(
3748 [f.LocalPath() for f in
3749 cl.GetChange(base_branch, None).AffectedFiles()],
3750 change.RepositoryRoot(), author,
3751 fopen=file, os_path=os.path, glob=glob.glob,
3752 disable_color=options.no_color).run()
3753
3754
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003755def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003756 """Generates a diff command."""
3757 # Generate diff for the current branch's changes.
3758 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
3759 upstream_commit, '--' ]
3760
3761 if args:
3762 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003763 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003764 diff_cmd.append(arg)
3765 else:
3766 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003767
3768 return diff_cmd
3769
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003770def MatchingFileType(file_name, extensions):
3771 """Returns true if the file name ends with one of the given extensions."""
3772 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003773
enne@chromium.org555cfe42014-01-29 18:21:39 +00003774@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003775def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003776 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00003777 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00003778 GN_EXTS = ['.gn', '.gni']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003779 parser.add_option('--full', action='store_true',
3780 help='Reformat the full content of all touched files')
3781 parser.add_option('--dry-run', action='store_true',
3782 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003783 parser.add_option('--python', action='store_true',
3784 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003785 parser.add_option('--diff', action='store_true',
3786 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003787 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003788
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003789 # git diff generates paths against the root of the repository. Change
3790 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003791 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003792 if rel_base_path:
3793 os.chdir(rel_base_path)
3794
digit@chromium.org29e47272013-05-17 17:01:46 +00003795 # Grab the merge-base commit, i.e. the upstream commit of the current
3796 # branch when it was created or the last time it was rebased. This is
3797 # to cover the case where the user may have called "git fetch origin",
3798 # moving the origin branch to a newer commit, but hasn't rebased yet.
3799 upstream_commit = None
3800 cl = Changelist()
3801 upstream_branch = cl.GetUpstreamBranch()
3802 if upstream_branch:
3803 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
3804 upstream_commit = upstream_commit.strip()
3805
3806 if not upstream_commit:
3807 DieWithError('Could not find base commit for this branch. '
3808 'Are you in detached state?')
3809
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003810 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
3811 diff_output = RunGit(changed_files_cmd)
3812 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00003813 # Filter out files deleted by this CL
3814 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003815
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003816 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
3817 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
3818 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00003819 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00003820
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003821 top_dir = os.path.normpath(
3822 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
3823
3824 # Locate the clang-format binary in the checkout
3825 try:
3826 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
3827 except clang_format.NotFoundError, e:
3828 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00003829
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003830 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
3831 # formatted. This is used to block during the presubmit.
3832 return_value = 0
3833
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003834 if clang_diff_files:
3835 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003836 cmd = [clang_format_tool]
3837 if not opts.dry_run and not opts.diff:
3838 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003839 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003840 if opts.diff:
3841 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003842 else:
3843 env = os.environ.copy()
3844 env['PATH'] = str(os.path.dirname(clang_format_tool))
3845 try:
3846 script = clang_format.FindClangFormatScriptInChromiumTree(
3847 'clang-format-diff.py')
3848 except clang_format.NotFoundError, e:
3849 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003850
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003851 cmd = [sys.executable, script, '-p0']
3852 if not opts.dry_run and not opts.diff:
3853 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003854
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003855 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
3856 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003857
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003858 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
3859 if opts.diff:
3860 sys.stdout.write(stdout)
3861 if opts.dry_run and len(stdout) > 0:
3862 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003863
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003864 # Similar code to above, but using yapf on .py files rather than clang-format
3865 # on C/C++ files
3866 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003867 yapf_tool = gclient_utils.FindExecutable('yapf')
3868 if yapf_tool is None:
3869 DieWithError('yapf not found in PATH')
3870
3871 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003872 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003873 cmd = [yapf_tool]
3874 if not opts.dry_run and not opts.diff:
3875 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003876 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003877 if opts.diff:
3878 sys.stdout.write(stdout)
3879 else:
3880 # TODO(sbc): yapf --lines mode still has some issues.
3881 # https://github.com/google/yapf/issues/154
3882 DieWithError('--python currently only works with --full')
3883
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003884 # Dart's formatter does not have the nice property of only operating on
3885 # modified chunks, so hard code full.
3886 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003887 try:
3888 command = [dart_format.FindDartFmtToolInChromiumTree()]
3889 if not opts.dry_run and not opts.diff:
3890 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003891 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003892
ppi@chromium.org6593d932016-03-03 15:41:15 +00003893 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003894 if opts.dry_run and stdout:
3895 return_value = 2
3896 except dart_format.NotFoundError as e:
erikcorry@chromium.org3e445022015-12-17 09:07:26 +00003897 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
3898 'found in this checkout. Files in other languages are still ' +
3899 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003900
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00003901 # Format GN build files. Always run on full build files for canonical form.
3902 if gn_diff_files:
3903 cmd = ['gn', 'format']
3904 if not opts.dry_run and not opts.diff:
3905 cmd.append('--in-place')
3906 for gn_diff_file in gn_diff_files:
3907 stdout = RunCommand(cmd + [gn_diff_file], cwd=top_dir)
3908 if opts.diff:
3909 sys.stdout.write(stdout)
3910
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003911 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003912
3913
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00003914@subcommand.usage('<codereview url or issue id>')
3915def CMDcheckout(parser, args):
3916 """Checks out a branch associated with a given Rietveld issue."""
3917 _, args = parser.parse_args(args)
3918
3919 if len(args) != 1:
3920 parser.print_help()
3921 return 1
3922
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003923 target_issue = ParseIssueNum(args[0])
3924 if target_issue == None:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00003925 parser.print_help()
3926 return 1
3927
3928 key_and_issues = [x.split() for x in RunGit(
3929 ['config', '--local', '--get-regexp', r'branch\..*\.rietveldissue'])
3930 .splitlines()]
3931 branches = []
3932 for key, issue in key_and_issues:
3933 if issue == target_issue:
3934 branches.append(re.sub(r'branch\.(.*)\.rietveldissue', r'\1', key))
3935
3936 if len(branches) == 0:
3937 print 'No branch found for issue %s.' % target_issue
3938 return 1
3939 if len(branches) == 1:
3940 RunGit(['checkout', branches[0]])
3941 else:
3942 print 'Multiple branches match issue %s:' % target_issue
3943 for i in range(len(branches)):
3944 print '%d: %s' % (i, branches[i])
3945 which = raw_input('Choose by index: ')
3946 try:
3947 RunGit(['checkout', branches[int(which)]])
3948 except (IndexError, ValueError):
3949 print 'Invalid selection, not checking out any branch.'
3950 return 1
3951
3952 return 0
3953
3954
maruel@chromium.org29404b52014-09-08 22:58:00 +00003955def CMDlol(parser, args):
3956 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00003957 print zlib.decompress(base64.b64decode(
3958 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
3959 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
3960 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
3961 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00003962 return 0
3963
3964
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003965class OptionParser(optparse.OptionParser):
3966 """Creates the option parse and add --verbose support."""
3967 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003968 optparse.OptionParser.__init__(
3969 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003970 self.add_option(
3971 '-v', '--verbose', action='count', default=0,
3972 help='Use 2 times for more debugging info')
3973
3974 def parse_args(self, args=None, values=None):
3975 options, args = optparse.OptionParser.parse_args(self, args, values)
3976 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
3977 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
3978 return options, args
3979
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003980
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003981def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00003982 if sys.hexversion < 0x02060000:
3983 print >> sys.stderr, (
3984 '\nYour python version %s is unsupported, please upgrade.\n' %
3985 sys.version.split(' ', 1)[0])
3986 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003987
maruel@chromium.orgddd59412011-11-30 14:20:38 +00003988 # Reload settings.
3989 global settings
3990 settings = Settings()
3991
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003992 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003993 dispatcher = subcommand.CommandDispatcher(__name__)
3994 try:
3995 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003996 except auth.AuthenticationError as e:
3997 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003998 except urllib2.HTTPError, e:
3999 if e.code != 500:
4000 raise
4001 DieWithError(
4002 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
4003 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00004004 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004005
4006
4007if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004008 # These affect sys.stdout so do it outside of main() to simplify mocks in
4009 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00004010 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004011 colorama.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00004012 try:
4013 sys.exit(main(sys.argv[1:]))
4014 except KeyboardInterrupt:
4015 sys.stderr.write('interrupted\n')
4016 sys.exit(1)