blob: 912601020f545065e2f12f07bc48473cd5e7b6ec [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
tandrii@chromium.org87985d22016-03-24 17:33:33 +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):
tandrii@chromium.org87985d22016-03-24 17:33:33 +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):
tandrii@chromium.org87985d22016-03-24 17:33:33 +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'."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +0000816 return branch.replace('refs/heads/', '')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000817
818
819class Changelist(object):
tandrii@chromium.org87985d22016-03-24 17:33:33 +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()
tandrii@chromium.org87985d22016-03-24 17:33:33 +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
tandrii@chromium.org87985d22016-03-24 17:33:33 +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 = ()
tandrii@chromium.org87985d22016-03-24 17:33:33 +0000842 self._auth_config = auth_config
843 self._props = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000844 self._remote = None
tandrii@chromium.org87985d22016-03-24 17:33:33 +0000845 self._rpc_server = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000846
tandrii@chromium.org87985d22016-03-24 17:33:33 +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:
tandrii@chromium.org87985d22016-03-24 17:33:33 +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:
tandrii@chromium.org87985d22016-03-24 17:33:33 +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:
1068 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
tandrii@chromium.org87985d22016-03-24 17:33:33 +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."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001095 if not self.GetIssue():
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001096 return None
tandrii@chromium.org87985d22016-03-24 17:33:33 +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():
tandrii@chromium.org87985d22016-03-24 17:33:33 +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:
tandrii@chromium.org87985d22016-03-24 17:33:33 +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:
tandrii@chromium.org87985d22016-03-24 17:33:33 +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:
tandrii@chromium.org87985d22016-03-24 17:33:33 +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
tandrii@chromium.org87985d22016-03-24 17:33:33 +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
tandrii@chromium.org87985d22016-03-24 17:33:33 +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:
tandrii@chromium.org87985d22016-03-24 17:33:33 +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
tandrii@chromium.org87985d22016-03-24 17:33:33 +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):
tandrii@chromium.org87985d22016-03-24 17:33:33 +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(
tandrii@chromium.org87985d22016-03-24 17:33:33 +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
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001329 def _IssueSetting(self):
tandrii@chromium.orgc4a94542016-03-24 17:33:00 +00001330 """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
tandrii@chromium.org87985d22016-03-24 17:33:33 +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
tandrii@chromium.org87985d22016-03-24 17:33:33 +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
tandrii@chromium.org87985d22016-03-24 17:33:33 +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."""
luqui@chromium.org609f3952015-05-04 22:47:04 +00002206 remote, remote_branch = cl.GetRemoteBranch()
2207 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2208 pending_prefix='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002209
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002210 change_desc = ChangeDescription(
2211 options.message or CreateDescriptionFromLog(args))
2212 if not change_desc.description:
andybons@chromium.org962f9462016-02-03 20:00:42 +00002213 print "\nDescription is empty. Aborting..."
2214 return 1
2215
2216 if options.title:
2217 print "\nPatch titles (-t) are not supported in Gerrit. Aborting..."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002218 return 1
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002219
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002220 if options.squash:
2221 # Try to get the message from a previous upload.
2222 shadow_branch = 'refs/heads/git_cl_uploads/' + cl.GetBranch()
bauerb@chromium.org13502e02016-02-18 10:18:29 +00002223 message = RunGitSilent(['show', '--format=%B', '-s', shadow_branch])
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002224 if not message:
2225 if not options.force:
2226 change_desc.prompt()
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002227 if not change_desc.description:
2228 print "Description is empty; aborting."
2229 return 1
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002230 message = change_desc.description
tandrii@chromium.org57d86542016-03-04 16:11:32 +00002231 change_ids = git_footers.get_footer_change_id(message)
2232 if len(change_ids) > 1:
2233 DieWithError('too many Change-Id footers in %s branch' % shadow_branch)
2234 if not change_ids:
2235 message = git_footers.add_footer_change_id(
2236 message, GenerateGerritChangeId(message))
2237 change_desc.set_description(message)
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002238 change_ids = git_footers.get_footer_change_id(message)
2239 assert len(change_ids) == 1
2240
2241 change_id = change_ids[0]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002242
2243 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2244 if remote is '.':
2245 # If our upstream branch is local, we base our squashed commit on its
2246 # squashed version.
2247 parent = ('refs/heads/git_cl_uploads/' +
2248 scm.GIT.ShortBranchName(upstream_branch))
2249
2250 # Verify that the upstream branch has been uploaded too, otherwise Gerrit
2251 # will create additional CLs when uploading.
2252 if (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2253 RunGitSilent(['rev-parse', parent + ':'])):
2254 print 'Upload upstream branch ' + upstream_branch + ' first.'
2255 return 1
2256 else:
2257 parent = cl.GetCommonAncestorWithUpstream()
2258
2259 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2260 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2261 '-m', message]).strip()
2262 else:
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00002263 if not git_footers.get_footer_change_id(change_desc.description):
tandrii@chromium.org10625002016-03-04 20:03:47 +00002264 DownloadGerritHook(False)
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002265 change_desc.set_description(AddChangeIdToCommitMessage(options, args))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002266 ref_to_push = 'HEAD'
alokp@chromium.org52eb5932016-03-25 04:35:53 +00002267 parent = '%s/%s' % (remote, branch)
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002268 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002269
2270 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2271 ref_to_push)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002272 if len(commits) > 1:
2273 print('WARNING: This will upload %d commits. Run the following command '
2274 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002275 print('git log %s..%s' % (parent, ref_to_push))
2276 print('You can also use `git squash-branch` to squash these into a single '
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002277 'commit.')
2278 ask_for_data('About to upload; enter to confirm.')
2279
piman@chromium.org336f9122014-09-04 02:16:55 +00002280 if options.reviewers or options.tbr_owners:
2281 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002282
ukai@chromium.orge8077812012-02-03 03:41:46 +00002283 receive_options = []
2284 cc = cl.GetCCList().split(',')
2285 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002286 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002287 cc = filter(None, cc)
2288 if cc:
2289 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002290 if change_desc.get_reviewers():
2291 receive_options.extend(
2292 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002293
ukai@chromium.orge8077812012-02-03 03:41:46 +00002294 git_command = ['push']
2295 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00002296 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00002297 ' '.join(receive_options))
alokp@chromium.org52eb5932016-03-25 04:35:53 +00002298 git_command += [remote, ref_to_push + ':refs/for/' + branch]
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002299 push_stdout = gclient_utils.CheckCallAndFilter(
2300 ['git'] + git_command,
2301 print_stdout=True,
2302 # Flush after every line: useful for seeing progress when running as
2303 # recipe.
2304 filter_fn=lambda _: sys.stdout.flush())
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002305
2306 if options.squash:
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002307 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2308 change_numbers = [m.group(1)
2309 for m in map(regex.match, push_stdout.splitlines())
2310 if m]
2311 if len(change_numbers) != 1:
2312 DieWithError(
2313 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2314 'Change-Id: %s') % (len(change_numbers), change_id))
2315 cl.SetIssue(change_numbers[0])
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002316 head = RunGit(['rev-parse', 'HEAD']).strip()
2317 RunGit(['update-ref', '-m', 'Uploaded ' + head, shadow_branch, ref_to_push])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002318 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002319
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002320
wittman@chromium.org455dc922015-01-26 20:15:50 +00002321def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
2322 """Computes the remote branch ref to use for the CL.
2323
2324 Args:
2325 remote (str): The git remote for the CL.
2326 remote_branch (str): The git remote branch for the CL.
2327 target_branch (str): The target branch specified by the user.
2328 pending_prefix (str): The pending prefix from the settings.
2329 """
2330 if not (remote and remote_branch):
2331 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002332
wittman@chromium.org455dc922015-01-26 20:15:50 +00002333 if target_branch:
2334 # Cannonicalize branch references to the equivalent local full symbolic
2335 # refs, which are then translated into the remote full symbolic refs
2336 # below.
2337 if '/' not in target_branch:
2338 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
2339 else:
2340 prefix_replacements = (
2341 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
2342 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
2343 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
2344 )
2345 match = None
2346 for regex, replacement in prefix_replacements:
2347 match = re.search(regex, target_branch)
2348 if match:
2349 remote_branch = target_branch.replace(match.group(0), replacement)
2350 break
2351 if not match:
2352 # This is a branch path but not one we recognize; use as-is.
2353 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00002354 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
2355 # Handle the refs that need to land in different refs.
2356 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002357
wittman@chromium.org455dc922015-01-26 20:15:50 +00002358 # Create the true path to the remote branch.
2359 # Does the following translation:
2360 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
2361 # * refs/remotes/origin/master -> refs/heads/master
2362 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
2363 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
2364 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
2365 elif remote_branch.startswith('refs/remotes/%s/' % remote):
2366 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
2367 'refs/heads/')
2368 elif remote_branch.startswith('refs/remotes/branch-heads'):
2369 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
2370 # If a pending prefix exists then replace refs/ with it.
2371 if pending_prefix:
2372 remote_branch = remote_branch.replace('refs/', pending_prefix)
2373 return remote_branch
2374
2375
piman@chromium.org336f9122014-09-04 02:16:55 +00002376def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002377 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002378 upload_args = ['--assume_yes'] # Don't ask about untracked files.
tandrii@chromium.org87985d22016-03-24 17:33:33 +00002379 upload_args.extend(['--server', cl.GetRietveldServer()])
2380 upload_args.extend(auth.auth_config_to_command_options(cl.auth_config))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002381 if options.emulate_svn_auto_props:
2382 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002383
2384 change_desc = None
2385
pgervais@chromium.org91141372014-01-09 23:27:20 +00002386 if options.email is not None:
2387 upload_args.extend(['--email', options.email])
2388
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002389 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002390 if options.title:
2391 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00002392 if options.message:
2393 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00002394 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002395 print ("This branch is associated with issue %s. "
2396 "Adding patch to that issue." % cl.GetIssue())
2397 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002398 if options.title:
2399 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00002400 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002401 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00002402 if options.reviewers or options.tbr_owners:
2403 change_desc.update_reviewers(options.reviewers,
2404 options.tbr_owners,
2405 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002406 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002407 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002408
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002409 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002410 print "Description is empty; aborting."
2411 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002412
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002413 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002414 if change_desc.get_reviewers():
2415 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00002416 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002417 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00002418 DieWithError("Must specify reviewers to send email.")
2419 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00002420
2421 # We check this before applying rietveld.private assuming that in
2422 # rietveld.cc only addresses which we can send private CLs to are listed
2423 # if rietveld.private is set, and so we should ignore rietveld.cc only when
2424 # --private is specified explicitly on the command line.
2425 if options.private:
2426 logging.warn('rietveld.cc is ignored since private flag is specified. '
2427 'You need to review and add them manually if necessary.')
2428 cc = cl.GetCCListWithoutDefault()
2429 else:
2430 cc = cl.GetCCList()
2431 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00002432 if cc:
2433 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002434
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002435 if options.private or settings.GetDefaultPrivateFlag() == "True":
2436 upload_args.append('--private')
2437
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002438 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00002439 if not options.find_copies:
2440 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002441
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002442 # Include the upstream repo's URL in the change -- this is useful for
2443 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002444 remote_url = cl.GetGitBaseUrlFromConfig()
2445 if not remote_url:
2446 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002447 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002448 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00002449 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
2450 remote_url = (cl.GetRemoteUrl() + '@'
2451 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002452 if remote_url:
2453 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00002454 remote, remote_branch = cl.GetRemoteBranch()
wittman@chromium.org455dc922015-01-26 20:15:50 +00002455 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2456 settings.GetPendingRefPrefix())
2457 if target_ref:
2458 upload_args.extend(['--target_ref', target_ref])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002459
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002460 # Look for dependent patchsets. See crbug.com/480453 for more details.
2461 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2462 upstream_branch = ShortBranchName(upstream_branch)
2463 if remote is '.':
2464 # A local branch is being tracked.
2465 local_branch = ShortBranchName(upstream_branch)
rmistry@google.com78948ed2015-07-08 23:09:57 +00002466 if settings.GetIsSkipDependencyUpload(local_branch):
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002467 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002468 print ('Skipping dependency patchset upload because git config '
2469 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002470 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002471 else:
2472 auth_config = auth.extract_auth_config_from_options(options)
2473 branch_cl = Changelist(branchref=local_branch, auth_config=auth_config)
2474 branch_cl_issue_url = branch_cl.GetIssueURL()
2475 branch_cl_issue = branch_cl.GetIssue()
2476 branch_cl_patchset = branch_cl.GetPatchset()
2477 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2478 upload_args.extend(
2479 ['--depends_on_patchset', '%s:%s' % (
2480 branch_cl_issue, branch_cl_patchset)])
2481 print
2482 print ('The current branch (%s) is tracking a local branch (%s) with '
2483 'an associated CL.') % (cl.GetBranch(), local_branch)
2484 print 'Adding %s/#ps%s as a dependency patchset.' % (
2485 branch_cl_issue_url, branch_cl_patchset)
2486 print
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002487
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002488 project = settings.GetProject()
2489 if project:
2490 upload_args.extend(['--project', project])
2491
rmistry@google.comef966222015-04-07 11:15:01 +00002492 if options.cq_dry_run:
2493 upload_args.extend(['--cq_dry_run'])
2494
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002495 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00002496 upload_args = ['upload'] + upload_args + args
2497 logging.info('upload.RealMain(%s)', upload_args)
2498 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00002499 issue = int(issue)
2500 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00002501 except KeyboardInterrupt:
2502 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002503 except:
2504 # If we got an exception after the user typed a description for their
2505 # change, back up the description before re-raising.
2506 if change_desc:
2507 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2508 print '\nGot exception while uploading -- saving description to %s\n' \
2509 % backup_path
2510 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002511 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002512 backup_file.close()
2513 raise
2514
2515 if not cl.GetIssue():
2516 cl.SetIssue(issue)
2517 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002518
2519 if options.use_commit_queue:
2520 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002521 return 0
2522
2523
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002524def cleanup_list(l):
2525 """Fixes a list so that comma separated items are put as individual items.
2526
2527 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
2528 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
2529 """
2530 items = sum((i.split(',') for i in l), [])
2531 stripped_items = (i.strip() for i in items)
2532 return sorted(filter(None, stripped_items))
2533
2534
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002535@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002536def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00002537 """Uploads the current changelist to codereview.
2538
2539 Can skip dependency patchset uploads for a branch by running:
2540 git config branch.branch_name.skip-deps-uploads True
2541 To unset run:
2542 git config --unset branch.branch_name.skip-deps-uploads
2543 Can also set the above globally by using the --global flag.
2544 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00002545 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2546 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002547 parser.add_option('--bypass-watchlists', action='store_true',
2548 dest='bypass_watchlists',
2549 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002550 parser.add_option('-f', action='store_true', dest='force',
2551 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002552 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00002553 parser.add_option('-t', dest='title',
2554 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002555 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002556 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002557 help='reviewer email addresses')
2558 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002559 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002560 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00002561 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00002562 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002563 parser.add_option('--emulate_svn_auto_props',
2564 '--emulate-svn-auto-props',
2565 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00002566 dest="emulate_svn_auto_props",
2567 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00002568 parser.add_option('-c', '--use-commit-queue', action='store_true',
2569 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002570 parser.add_option('--private', action='store_true',
2571 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00002572 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002573 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00002574 metavar='TARGET',
2575 help='Apply CL to remote ref TARGET. ' +
2576 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002577 parser.add_option('--squash', action='store_true',
2578 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002579 parser.add_option('--no-squash', action='store_true',
2580 help='Don\'t squash multiple commits into one ' +
2581 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002582 parser.add_option('--email', default=None,
2583 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00002584 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
2585 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00002586 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
2587 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00002588 help='Send the patchset to do a CQ dry run right after '
2589 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00002590 parser.add_option('--dependencies', action='store_true',
2591 help='Uploads CLs of all the local branches that depend on '
2592 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002593
rmistry@google.com2dd99862015-06-22 12:22:18 +00002594 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002595 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002596 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002597 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002598 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002599
sbc@chromium.org71437c02015-04-09 19:29:40 +00002600 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002601 return 1
2602
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002603 options.reviewers = cleanup_list(options.reviewers)
2604 options.cc = cleanup_list(options.cc)
2605
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002606 cl = Changelist(auth_config=auth_config)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002607 if args:
2608 # TODO(ukai): is it ok for gerrit case?
2609 base_branch = args[0]
2610 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00002611 if cl.GetBranch() is None:
2612 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
2613
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002614 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002615 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00002616 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00002617
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00002618 # Make sure authenticated to Rietveld before running expensive hooks. It is
2619 # a fast, best efforts check. Rietveld still can reject the authentication
2620 # during the actual upload.
2621 if not settings.GetIsGerrit() and auth_config.use_oauth2:
2622 authenticator = auth.get_authenticator_for_host(
tandrii@chromium.org87985d22016-03-24 17:33:33 +00002623 cl.GetRietveldServer(), auth_config)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00002624 if not authenticator.has_cached_credentials():
tandrii@chromium.org87985d22016-03-24 17:33:33 +00002625 raise auth.LoginRequiredError(cl.GetRietveldServer())
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00002626
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002627 # Apply watchlists on upload.
2628 change = cl.GetChange(base_branch, None)
2629 watchlist = watchlists.Watchlists(change.RepositoryRoot())
2630 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002631 if not options.bypass_watchlists:
2632 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002633
ukai@chromium.orge8077812012-02-03 03:41:46 +00002634 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00002635 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002636 # Set the reviewer list now so that presubmit checks can access it.
2637 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00002638 change_description.update_reviewers(options.reviewers,
2639 options.tbr_owners,
2640 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002641 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002642 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00002643 may_prompt=not options.force,
2644 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002645 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002646 if not hook_results.should_continue():
2647 return 1
2648 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002649 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002650
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002651 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002652 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002653 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00002654 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002655 print ('The last upload made from this repository was patchset #%d but '
2656 'the most recent patchset on the server is #%d.'
2657 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00002658 print ('Uploading will still work, but if you\'ve uploaded to this issue '
2659 'from another machine or branch the patch you\'re uploading now '
2660 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002661 ask_for_data('About to upload; enter to confirm.')
2662
iannucci@chromium.org79540052012-10-19 23:15:26 +00002663 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002664 if settings.GetIsGerrit():
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002665 if options.squash and options.no_squash:
2666 DieWithError('Can only use one of --squash or --no-squash')
2667
2668 options.squash = ((settings.GetSquashGerritUploads() or options.squash) and
2669 not options.no_squash)
2670
tandrii@chromium.org1e67bb72016-02-11 12:15:49 +00002671 ret = GerritUpload(options, args, cl, change)
2672 else:
2673 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002674 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00002675 git_set_branch_value('last-upload-hash',
2676 RunGit(['rev-parse', 'HEAD']).strip())
rmistry@google.com5626a922015-02-26 14:03:30 +00002677 # Run post upload hooks, if specified.
2678 if settings.GetRunPostUploadHook():
2679 presubmit_support.DoPostUploadExecuter(
2680 change,
2681 cl,
2682 settings.GetRoot(),
2683 options.verbose,
2684 sys.stdout)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002685
rmistry@google.com2dd99862015-06-22 12:22:18 +00002686 # Upload all dependencies if specified.
2687 if options.dependencies:
2688 print
2689 print '--dependencies has been specified.'
2690 print 'All dependent local branches will be re-uploaded.'
2691 print
2692 # Remove the dependencies flag from args so that we do not end up in a
2693 # loop.
2694 orig_args.remove('--dependencies')
2695 upload_branch_deps(cl, orig_args)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002696 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00002697
2698
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002699def IsSubmoduleMergeCommit(ref):
2700 # When submodules are added to the repo, we expect there to be a single
2701 # non-git-svn merge commit at remote HEAD with a signature comment.
2702 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00002703 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002704 return RunGit(cmd) != ''
2705
2706
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002707def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002708 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002709
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002710 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002711 Updates changelog with metadata (e.g. pointer to review).
2712 Pushes/dcommits the code upstream.
2713 Updates review and closes.
2714 """
2715 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2716 help='bypass upload presubmit hook')
2717 parser.add_option('-m', dest='message',
2718 help="override review description")
2719 parser.add_option('-f', action='store_true', dest='force',
2720 help="force yes to questions (don't prompt)")
2721 parser.add_option('-c', dest='contributor',
2722 help="external contributor for patch (appended to " +
2723 "description and used as author for git). Should be " +
2724 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002725 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002726 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002727 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002728 auth_config = auth.extract_auth_config_from_options(options)
2729
2730 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002731
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002732 current = cl.GetBranch()
2733 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2734 if not settings.GetIsGitSvn() and remote == '.':
2735 print
2736 print 'Attempting to push branch %r into another local branch!' % current
2737 print
2738 print 'Either reparent this branch on top of origin/master:'
2739 print ' git reparent-branch --root'
2740 print
2741 print 'OR run `git rebase-update` if you think the parent branch is already'
2742 print 'committed.'
2743 print
2744 print ' Current parent: %r' % upstream_branch
2745 return 1
2746
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002747 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002748 # Default to merging against our best guess of the upstream branch.
2749 args = [cl.GetUpstreamBranch()]
2750
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002751 if options.contributor:
2752 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
2753 print "Please provide contibutor as 'First Last <email@example.com>'"
2754 return 1
2755
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002756 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002757 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002758
sbc@chromium.org71437c02015-04-09 19:29:40 +00002759 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002760 return 1
2761
2762 # This rev-list syntax means "show all commits not in my branch that
2763 # are in base_branch".
2764 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
2765 base_branch]).splitlines()
2766 if upstream_commits:
2767 print ('Base branch "%s" has %d commits '
2768 'not in this branch.' % (base_branch, len(upstream_commits)))
2769 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
2770 return 1
2771
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002772 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002773 svn_head = None
2774 if cmd == 'dcommit' or base_has_submodules:
2775 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
2776 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002777
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002778 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002779 # If the base_head is a submodule merge commit, the first parent of the
2780 # base_head should be a git-svn commit, which is what we're interested in.
2781 base_svn_head = base_branch
2782 if base_has_submodules:
2783 base_svn_head += '^1'
2784
2785 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002786 if extra_commits:
2787 print ('This branch has %d additional commits not upstreamed yet.'
2788 % len(extra_commits.splitlines()))
2789 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
2790 'before attempting to %s.' % (base_branch, cmd))
2791 return 1
2792
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002793 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002794 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002795 author = None
2796 if options.contributor:
2797 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002798 hook_results = cl.RunHook(
2799 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002800 may_prompt=not options.force,
2801 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002802 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002803 if not hook_results.should_continue():
2804 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002805
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002806 # Check the tree status if the tree status URL is set.
2807 status = GetTreeStatus()
2808 if 'closed' == status:
2809 print('The tree is closed. Please wait for it to reopen. Use '
2810 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2811 return 1
2812 elif 'unknown' == status:
2813 print('Unable to determine tree status. Please verify manually and '
2814 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2815 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002816
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002817 change_desc = ChangeDescription(options.message)
2818 if not change_desc.description and cl.GetIssue():
2819 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002820
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002821 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00002822 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002823 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00002824 else:
2825 print 'No description set.'
2826 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
2827 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002828
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002829 # Keep a separate copy for the commit message, because the commit message
2830 # contains the link to the Rietveld issue, while the Rietveld message contains
2831 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002832 # Keep a separate copy for the commit message.
2833 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00002834 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002835
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002836 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00002837 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00002838 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00002839 # after it. Add a period on a new line to circumvent this. Also add a space
2840 # before the period to make sure that Gitiles continues to correctly resolve
2841 # the URL.
2842 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002843 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002844 commit_desc.append_footer('Patch from %s.' % options.contributor)
2845
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00002846 print('Description:')
2847 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002848
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002849 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002850 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00002851 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002852
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002853 # We want to squash all this branch's commits into one commit with the proper
2854 # description. We do this by doing a "reset --soft" to the base branch (which
2855 # keeps the working copy the same), then dcommitting that. If origin/master
2856 # has a submodule merge commit, we'll also need to cherry-pick the squashed
2857 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002858 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002859 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
2860 # Delete the branches if they exist.
2861 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
2862 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
2863 result = RunGitWithCode(showref_cmd)
2864 if result[0] == 0:
2865 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002866
2867 # We might be in a directory that's present in this branch but not in the
2868 # trunk. Move up to the top of the tree so that git commands that expect a
2869 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002870 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002871 if rel_base_path:
2872 os.chdir(rel_base_path)
2873
2874 # Stuff our change into the merge branch.
2875 # We wrap in a try...finally block so if anything goes wrong,
2876 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002877 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002878 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002879 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002880 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002881 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00002882 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002883 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002884 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002885 RunGit(
2886 [
2887 'commit', '--author', options.contributor,
2888 '-m', commit_desc.description,
2889 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002890 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002891 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002892 if base_has_submodules:
2893 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
2894 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
2895 RunGit(['checkout', CHERRY_PICK_BRANCH])
2896 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002897 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002898 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00002899 mirror = settings.GetGitMirror(remote)
2900 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002901 pending_prefix = settings.GetPendingRefPrefix()
2902 if not pending_prefix or branch.startswith(pending_prefix):
2903 # If not using refs/pending/heads/* at all, or target ref is already set
2904 # to pending, then push to the target ref directly.
2905 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00002906 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002907 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002908 else:
2909 # Cherry-pick the change on top of pending ref and then push it.
2910 assert branch.startswith('refs/'), branch
2911 assert pending_prefix[-1] == '/', pending_prefix
2912 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00002913 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002914 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002915 if retcode == 0:
2916 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002917 else:
2918 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002919 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002920 'svn', 'dcommit',
2921 '-C%s' % options.similarity,
2922 '--no-rebase', '--rmdir',
2923 ]
2924 if settings.GetForceHttpsCommitUrl():
2925 # Allow forcing https commit URLs for some projects that don't allow
2926 # committing to http URLs (like Google Code).
2927 remote_url = cl.GetGitSvnRemoteUrl()
2928 if urlparse.urlparse(remote_url).scheme == 'http':
2929 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002930 cmd_args.append('--commit-url=%s' % remote_url)
2931 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002932 if 'Committed r' in output:
2933 revision = re.match(
2934 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2935 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002936 finally:
2937 # And then swap back to the original branch and clean up.
2938 RunGit(['checkout', '-q', cl.GetBranch()])
2939 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002940 if base_has_submodules:
2941 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002942
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002943 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002944 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002945 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002946
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002947 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002948 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002949 try:
2950 revision = WaitForRealCommit(remote, revision, base_branch, branch)
2951 # We set pushed_to_pending to False, since it made it all the way to the
2952 # real ref.
2953 pushed_to_pending = False
2954 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002955 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002956
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002957 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002958 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002959 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002960 if not to_pending:
2961 if viewvc_url and revision:
2962 change_desc.append_footer(
2963 'Committed: %s%s' % (viewvc_url, revision))
2964 elif revision:
2965 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002966 print ('Closing issue '
2967 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002968 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002969 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002970 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002971 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00002972 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00002973 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002974 if options.bypass_hooks:
2975 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2976 else:
2977 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002978 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002979 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002980
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002981 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002982 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
2983 print 'The commit is in the pending queue (%s).' % pending_ref
2984 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00002985 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002986 'footer.' % branch)
2987
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002988 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2989 if os.path.isfile(hook):
2990 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002991
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002992 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002993
2994
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002995def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
2996 print
2997 print 'Waiting for commit to be landed on %s...' % real_ref
2998 print '(If you are impatient, you may Ctrl-C once without harm)'
2999 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
3000 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003001 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003002
3003 loop = 0
3004 while True:
3005 sys.stdout.write('fetching (%d)... \r' % loop)
3006 sys.stdout.flush()
3007 loop += 1
3008
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003009 if mirror:
3010 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003011 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
3012 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
3013 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
3014 for commit in commits.splitlines():
3015 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
3016 print 'Found commit on %s' % real_ref
3017 return commit
3018
3019 current_rev = to_rev
3020
3021
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003022def PushToGitPending(remote, pending_ref, upstream_ref):
3023 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
3024
3025 Returns:
3026 (retcode of last operation, output log of last operation).
3027 """
3028 assert pending_ref.startswith('refs/'), pending_ref
3029 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
3030 cherry = RunGit(['rev-parse', 'HEAD']).strip()
3031 code = 0
3032 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003033 max_attempts = 3
3034 attempts_left = max_attempts
3035 while attempts_left:
3036 if attempts_left != max_attempts:
3037 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
3038 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003039
3040 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003041 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003042 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003043 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003044 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003045 print 'Fetch failed with exit code %d.' % code
3046 if out.strip():
3047 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003048 continue
3049
3050 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003051 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003052 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003053 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003054 if code:
3055 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003056 'Your patch doesn\'t apply cleanly to ref \'%s\', '
3057 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003058 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
3059 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003060 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003061 return code, out
3062
3063 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003064 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003065 code, out = RunGitWithCode(
3066 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
3067 if code == 0:
3068 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003069 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003070 return code, out
3071
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003072 print 'Push failed with exit code %d.' % code
3073 if out.strip():
3074 print out.strip()
3075 if IsFatalPushFailure(out):
3076 print (
3077 'Fatal push error. Make sure your .netrc credentials and git '
3078 'user.email are correct and you have push access to the repo.')
3079 return code, out
3080
3081 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003082 return code, out
3083
3084
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003085def IsFatalPushFailure(push_stdout):
3086 """True if retrying push won't help."""
3087 return '(prohibited by Gerrit)' in push_stdout
3088
3089
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003090@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003091def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003092 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003093 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003094 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003095 # If it looks like previous commits were mirrored with git-svn.
3096 message = """This repository appears to be a git-svn mirror, but no
3097upstream SVN master is set. You probably need to run 'git auto-svn' once."""
3098 else:
3099 message = """This doesn't appear to be an SVN repository.
3100If your project has a true, writeable git repository, you probably want to run
3101'git cl land' instead.
3102If your project has a git mirror of an upstream SVN master, you probably need
3103to run 'git svn init'.
3104
3105Using the wrong command might cause your commit to appear to succeed, and the
3106review to be closed, without actually landing upstream. If you choose to
3107proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00003108 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00003109 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003110 return SendUpstream(parser, args, 'dcommit')
3111
3112
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003113@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003114def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003115 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003116 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003117 print('This appears to be an SVN repository.')
3118 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003119 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00003120 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003121 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003122
3123
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003124def ParseIssueNum(arg):
3125 """Parses the issue number from args if present otherwise returns None."""
3126 if re.match(r'\d+', arg):
3127 return arg
3128 if arg.startswith('http'):
3129 return re.sub(r'.*/(\d+)/?', r'\1', arg)
3130 return None
3131
3132
3133@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003134def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00003135 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003136 parser.add_option('-b', dest='newbranch',
3137 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003138 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003139 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003140 parser.add_option('-d', '--directory', action='store', metavar='DIR',
3141 help='Change to the directory DIR immediately, '
3142 'before doing anything else.')
3143 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003144 help='failed patches spew .rej files rather than '
3145 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003146 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
3147 help="don't commit after patch applies")
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003148
3149 group = optparse.OptionGroup(parser,
3150 """Options for continuing work on the current issue uploaded
3151from a different clone (e.g. different machine). Must be used independently from
3152the other options. No issue number should be specified, and the branch must have
3153an issue number associated with it""")
3154 group.add_option('--reapply', action='store_true',
3155 dest='reapply',
3156 help="""Reset the branch and reapply the issue.
3157CAUTION: This will undo any local changes in this branch""")
3158
3159 group.add_option('--pull', action='store_true', dest='pull',
3160 help="Performs a pull before reapplying.")
3161 parser.add_option_group(group)
3162
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003163 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003164 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003165 auth_config = auth.extract_auth_config_from_options(options)
3166
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003167 issue_arg = None
3168 if options.reapply :
3169 if len(args) > 0:
3170 parser.error("--reapply implies no additional arguments.")
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003171
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003172 cl = Changelist()
3173 issue_arg = cl.GetIssue()
3174 upstream = cl.GetUpstreamBranch()
3175 if upstream == None:
3176 parser.error("No upstream branch specified. Cannot reset branch")
3177
3178 RunGit(['reset', '--hard', upstream])
3179 if options.pull:
3180 RunGit(['pull'])
3181 else:
3182 if len(args) != 1:
3183 parser.error("Must specify issue number")
3184
3185 issue_arg = ParseIssueNum(args[0])
3186
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003187 # The patch URL works because ParseIssueNum won't do any substitution
3188 # as the re.sub pattern fails to match and just returns it.
3189 if issue_arg == None:
3190 parser.print_help()
3191 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003192
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003193 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003194 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003195 return 1
3196
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003197 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00003198 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003199
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003200 if options.newbranch:
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003201 if options.reapply:
3202 parser.error("--reapply excludes any option other than --pull")
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003203 if options.force:
3204 RunGit(['branch', '-D', options.newbranch],
3205 stderr=subprocess2.PIPE, error_ok=True)
3206 RunGit(['checkout', '-b', options.newbranch,
3207 Changelist().GetUpstreamBranch()])
3208
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003209 return PatchIssue(issue_arg, options.reject, options.nocommit,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003210 options.directory, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003211
3212
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003213def PatchIssue(issue_arg, reject, nocommit, directory, auth_config):
wychen@chromium.orga872e752015-04-28 23:42:18 +00003214 # PatchIssue should never be called with a dirty tree. It is up to the
3215 # caller to check this, but just in case we assert here since the
3216 # consequences of the caller not checking this could be dire.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003217 assert(not git_common.is_dirty_git_tree('apply'))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003218
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003219 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003220 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00003221 issue = int(issue_arg)
tandrii@chromium.org87985d22016-03-24 17:33:33 +00003222 cl = Changelist(issue=issue, auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003223 patchset = cl.GetMostRecentPatchset()
tandrii@chromium.org87985d22016-03-24 17:33:33 +00003224 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003225 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00003226 # Assume it's a URL to the patch. Default to https.
3227 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
kjellander@chromium.org44424542015-06-02 18:35:29 +00003228 match = re.match(r'(.*?)/download/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00003229 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003230 DieWithError('Must pass an issue ID or full URL for '
3231 '\'Download raw patch set\'')
kjellander@chromium.org44424542015-06-02 18:35:29 +00003232 issue = int(match.group(2))
tandrii@chromium.org87985d22016-03-24 17:33:33 +00003233 cl = Changelist(issue=issue, auth_config=auth_config)
3234 cl.rietveld_server = match.group(1)
kjellander@chromium.org44424542015-06-02 18:35:29 +00003235 patchset = int(match.group(3))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00003236 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003237
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003238 # Switch up to the top-level directory, if necessary, in preparation for
3239 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003240 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003241 if top:
3242 os.chdir(top)
3243
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003244 # Git patches have a/ at the beginning of source paths. We strip that out
3245 # with a sed script rather than the -p flag to patch so we can feed either
3246 # Git or svn-style patches into the same apply command.
3247 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003248 try:
3249 patch_data = subprocess2.check_output(
3250 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
3251 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003252 DieWithError('Git patch mungling failed.')
3253 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003254
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003255 # We use "git apply" to apply the patch instead of "patch" so that we can
3256 # pick up file adds.
3257 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003258 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003259 if directory:
3260 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003261 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003262 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003263 elif IsGitVersionAtLeast('1.7.12'):
3264 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003265 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003266 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003267 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003268 except subprocess2.CalledProcessError:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003269 print 'Failed to apply the patch'
3270 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003271
3272 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003273 if not nocommit:
wychen@chromium.org5b3bebb2015-05-28 21:41:43 +00003274 RunGit(['commit', '-m', (cl.GetDescription() + '\n\n' +
3275 'patch from issue %(i)s at patchset '
carlosk@chromium.org71284d92014-11-14 18:12:50 +00003276 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
3277 % {'i': issue, 'p': patchset})])
tandrii@chromium.org87985d22016-03-24 17:33:33 +00003278 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003279 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00003280 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00003281 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003282 else:
3283 print "Patch applied to index."
3284 return 0
3285
3286
3287def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003288 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003289 # Provide a wrapper for git svn rebase to help avoid accidental
3290 # git svn dcommit.
3291 # It's the only command that doesn't use parser at all since we just defer
3292 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003293
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003294 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003295
3296
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003297def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003298 """Fetches the tree status and returns either 'open', 'closed',
3299 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003300 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003301 if url:
3302 status = urllib2.urlopen(url).read().lower()
3303 if status.find('closed') != -1 or status == '0':
3304 return 'closed'
3305 elif status.find('open') != -1 or status == '1':
3306 return 'open'
3307 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003308 return 'unset'
3309
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003310
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003311def GetTreeStatusReason():
3312 """Fetches the tree status from a json url and returns the message
3313 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00003314 url = settings.GetTreeStatusUrl()
3315 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003316 connection = urllib2.urlopen(json_url)
3317 status = json.loads(connection.read())
3318 connection.close()
3319 return status['message']
3320
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003321
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003322def GetBuilderMaster(bot_list):
3323 """For a given builder, fetch the master from AE if available."""
3324 map_url = 'https://builders-map.appspot.com/'
3325 try:
3326 master_map = json.load(urllib2.urlopen(map_url))
3327 except urllib2.URLError as e:
3328 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
3329 (map_url, e))
3330 except ValueError as e:
3331 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
3332 if not master_map:
3333 return None, 'Failed to build master map.'
3334
3335 result_master = ''
3336 for bot in bot_list:
3337 builder = bot.split(':', 1)[0]
3338 master_list = master_map.get(builder, [])
3339 if not master_list:
3340 return None, ('No matching master for builder %s.' % builder)
3341 elif len(master_list) > 1:
3342 return None, ('The builder name %s exists in multiple masters %s.' %
3343 (builder, master_list))
3344 else:
3345 cur_master = master_list[0]
3346 if not result_master:
3347 result_master = cur_master
3348 elif result_master != cur_master:
3349 return None, 'The builders do not belong to the same master.'
3350 return result_master, None
3351
3352
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003353def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003354 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003355 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003356 status = GetTreeStatus()
3357 if 'unset' == status:
3358 print 'You must configure your tree status URL by running "git cl config".'
3359 return 2
3360
3361 print "The tree is %s" % status
3362 print
3363 print GetTreeStatusReason()
3364 if status != 'open':
3365 return 1
3366 return 0
3367
3368
maruel@chromium.org15192402012-09-06 12:38:29 +00003369def CMDtry(parser, args):
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003370 """Triggers a try job through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00003371 group = optparse.OptionGroup(parser, "Try job options")
3372 group.add_option(
3373 "-b", "--bot", action="append",
3374 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
3375 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003376 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00003377 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003378 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00003379 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003380 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00003381 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003382 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003383 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003384 "-r", "--revision",
3385 help="Revision to use for the try job; default: the "
3386 "revision will be determined by the try server; see "
3387 "its waterfall for more info")
3388 group.add_option(
3389 "-c", "--clobber", action="store_true", default=False,
3390 help="Force a clobber before building; e.g. don't do an "
3391 "incremental build")
3392 group.add_option(
3393 "--project",
3394 help="Override which project to use. Projects are defined "
3395 "server-side to define what default bot set to use")
3396 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00003397 "-p", "--property", dest="properties", action="append", default=[],
3398 help="Specify generic properties in the form -p key1=value1 -p "
3399 "key2=value2 etc (buildbucket only). The value will be treated as "
3400 "json if decodable, or as string otherwise.")
3401 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003402 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003403 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003404 "--use-rietveld", action="store_true", default=False,
3405 help="Use Rietveld to trigger try jobs.")
3406 group.add_option(
3407 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3408 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00003409 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003410 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00003411 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003412 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00003413
machenbach@chromium.org45453142015-09-15 08:45:22 +00003414 if options.use_rietveld and options.properties:
3415 parser.error('Properties can only be specified with buildbucket')
3416
3417 # Make sure that all properties are prop=value pairs.
3418 bad_params = [x for x in options.properties if '=' not in x]
3419 if bad_params:
3420 parser.error('Got properties with missing "=": %s' % bad_params)
3421
maruel@chromium.org15192402012-09-06 12:38:29 +00003422 if args:
3423 parser.error('Unknown arguments: %s' % args)
3424
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003425 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00003426 if not cl.GetIssue():
3427 parser.error('Need to upload first')
3428
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003429 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00003430 if props.get('closed'):
3431 parser.error('Cannot send tryjobs for a closed CL')
3432
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003433 if props.get('private'):
3434 parser.error('Cannot use trybots with private issue')
3435
maruel@chromium.org15192402012-09-06 12:38:29 +00003436 if not options.name:
3437 options.name = cl.GetBranch()
3438
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003439 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003440 options.master, err_msg = GetBuilderMaster(options.bot)
3441 if err_msg:
3442 parser.error('Tryserver master cannot be found because: %s\n'
3443 'Please manually specify the tryserver master'
3444 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003445
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003446 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003447 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003448 if not options.bot:
3449 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00003450
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003451 # Get try masters from PRESUBMIT.py files.
3452 masters = presubmit_support.DoGetTryMasters(
3453 change,
3454 change.LocalPaths(),
3455 settings.GetRoot(),
3456 None,
3457 None,
3458 options.verbose,
3459 sys.stdout)
3460 if masters:
3461 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00003462
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003463 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
3464 options.bot = presubmit_support.DoGetTrySlaves(
3465 change,
3466 change.LocalPaths(),
3467 settings.GetRoot(),
3468 None,
3469 None,
3470 options.verbose,
3471 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00003472
3473 if not options.bot:
3474 # Get try masters from cq.cfg if any.
3475 # TODO(tandrii): some (but very few) projects store cq.cfg in different
3476 # location.
3477 cq_cfg = os.path.join(change.RepositoryRoot(),
3478 'infra', 'config', 'cq.cfg')
3479 if os.path.exists(cq_cfg):
3480 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00003481 cq_masters = commit_queue.get_master_builder_map(
3482 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00003483 for master, builders in cq_masters.iteritems():
3484 for builder in builders:
3485 # Skip presubmit builders, because these will fail without LGTM.
3486 if 'presubmit' not in builder.lower():
3487 masters.setdefault(master, {})[builder] = ['defaulttests']
3488 if masters:
3489 return masters
3490
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003491 if not options.bot:
3492 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00003493
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003494 builders_and_tests = {}
3495 # TODO(machenbach): The old style command-line options don't support
3496 # multiple try masters yet.
3497 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
3498 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
3499
3500 for bot in old_style:
3501 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003502 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003503 elif ',' in bot:
3504 parser.error('Specify one bot per --bot flag')
3505 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00003506 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003507
3508 for bot, tests in new_style:
3509 builders_and_tests.setdefault(bot, []).extend(tests)
3510
3511 # Return a master map with one master to be backwards compatible. The
3512 # master name defaults to an empty string, which will cause the master
3513 # not to be set on rietveld (deprecated).
3514 return {options.master: builders_and_tests}
3515
3516 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00003517
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003518 for builders in masters.itervalues():
3519 if any('triggered' in b for b in builders):
3520 print >> sys.stderr, (
3521 'ERROR You are trying to send a job to a triggered bot. This type of'
3522 ' bot requires an\ninitial job from a parent (usually a builder). '
3523 'Instead send your job to the parent.\n'
3524 'Bot list: %s' % builders)
3525 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00003526
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00003527 patchset = cl.GetMostRecentPatchset()
3528 if patchset and patchset != cl.GetPatchset():
3529 print(
3530 '\nWARNING Mismatch between local config and server. Did a previous '
3531 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3532 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003533 if options.luci:
3534 trigger_luci_job(cl, masters, options)
3535 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003536 try:
3537 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
3538 except BuildbucketResponseException as ex:
3539 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00003540 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003541 except Exception as e:
3542 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3543 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
3544 e, stacktrace)
3545 return 1
3546 else:
3547 try:
3548 cl.RpcServer().trigger_distributed_try_jobs(
3549 cl.GetIssue(), patchset, options.name, options.clobber,
3550 options.revision, masters)
3551 except urllib2.HTTPError as e:
3552 if e.code == 404:
3553 print('404 from rietveld; '
3554 'did you mean to use "git try" instead of "git cl try"?')
3555 return 1
3556 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003557
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003558 for (master, builders) in sorted(masters.iteritems()):
3559 if master:
3560 print 'Master: %s' % master
3561 length = max(len(builder) for builder in builders)
3562 for builder in sorted(builders):
3563 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00003564 return 0
3565
3566
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00003567def CMDtry_results(parser, args):
3568 group = optparse.OptionGroup(parser, "Try job results options")
3569 group.add_option(
3570 "-p", "--patchset", type=int, help="patchset number if not current.")
3571 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00003572 "--print-master", action='store_true', help="print master name as well.")
3573 group.add_option(
3574 "--color", action='store_true', default=sys.stdout.isatty(),
3575 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00003576 group.add_option(
3577 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3578 help="Host of buildbucket. The default host is %default.")
3579 parser.add_option_group(group)
3580 auth.add_auth_options(parser)
3581 options, args = parser.parse_args(args)
3582 if args:
3583 parser.error('Unrecognized args: %s' % ' '.join(args))
3584
3585 auth_config = auth.extract_auth_config_from_options(options)
3586 cl = Changelist(auth_config=auth_config)
3587 if not cl.GetIssue():
3588 parser.error('Need to upload first')
3589
3590 if not options.patchset:
3591 options.patchset = cl.GetMostRecentPatchset()
3592 if options.patchset and options.patchset != cl.GetPatchset():
3593 print(
3594 '\nWARNING Mismatch between local config and server. Did a previous '
3595 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3596 'Continuing using\npatchset %s.\n' % options.patchset)
3597 try:
3598 jobs = fetch_try_jobs(auth_config, cl, options)
3599 except BuildbucketResponseException as ex:
3600 print 'Buildbucket error: %s' % ex
3601 return 1
3602 except Exception as e:
3603 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3604 print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
3605 e, stacktrace)
3606 return 1
3607 print_tryjobs(options, jobs)
3608 return 0
3609
3610
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003611@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003612def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003613 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003614 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003615 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003616 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003617
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003618 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003619 if args:
3620 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003621 branch = cl.GetBranch()
3622 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003623 cl = Changelist()
3624 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003625
3626 # Clear configured merge-base, if there is one.
3627 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003628 else:
3629 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003630 return 0
3631
3632
thestig@chromium.org00858c82013-12-02 23:08:03 +00003633def CMDweb(parser, args):
3634 """Opens the current CL in the web browser."""
3635 _, args = parser.parse_args(args)
3636 if args:
3637 parser.error('Unrecognized args: %s' % ' '.join(args))
3638
3639 issue_url = Changelist().GetIssueURL()
3640 if not issue_url:
3641 print >> sys.stderr, 'ERROR No issue to open'
3642 return 1
3643
3644 webbrowser.open(issue_url)
3645 return 0
3646
3647
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003648def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003649 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003650 auth.add_auth_options(parser)
3651 options, args = parser.parse_args(args)
3652 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003653 if args:
3654 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003655 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003656 props = cl.GetIssueProperties()
3657 if props.get('private'):
3658 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003659 cl.SetFlag('commit', '1')
3660 return 0
3661
3662
groby@chromium.org411034a2013-02-26 15:12:01 +00003663def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003664 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003665 auth.add_auth_options(parser)
3666 options, args = parser.parse_args(args)
3667 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00003668 if args:
3669 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003670 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00003671 # Ensure there actually is an issue to close.
3672 cl.GetDescription()
3673 cl.CloseIssue()
3674 return 0
3675
3676
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003677def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003678 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003679 auth.add_auth_options(parser)
3680 options, args = parser.parse_args(args)
3681 auth_config = auth.extract_auth_config_from_options(options)
3682 if args:
3683 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003684
3685 # Uncommitted (staged and unstaged) changes will be destroyed by
3686 # "git reset --hard" if there are merging conflicts in PatchIssue().
3687 # Staged changes would be committed along with the patch from last
3688 # upload, hence counted toward the "last upload" side in the final
3689 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003690 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003691 return 1
3692
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003693 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003694 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003695 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003696 if not issue:
3697 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003698 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003699 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003700
3701 # Create a new branch based on the merge-base
3702 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
3703 try:
3704 # Patch in the latest changes from rietveld.
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003705 rtn = PatchIssue(issue, False, False, None, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003706 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003707 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003708 return rtn
3709
wychen@chromium.org06928532015-02-03 02:11:29 +00003710 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003711 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00003712 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003713 finally:
3714 RunGit(['checkout', '-q', branch])
3715 RunGit(['branch', '-D', TMP_BRANCH])
3716
3717 return 0
3718
3719
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003720def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003721 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003722 parser.add_option(
3723 '--no-color',
3724 action='store_true',
3725 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003726 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003727 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003728 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003729
3730 author = RunGit(['config', 'user.email']).strip() or None
3731
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003732 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003733
3734 if args:
3735 if len(args) > 1:
3736 parser.error('Unknown args')
3737 base_branch = args[0]
3738 else:
3739 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003740 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003741
3742 change = cl.GetChange(base_branch, None)
3743 return owners_finder.OwnersFinder(
3744 [f.LocalPath() for f in
3745 cl.GetChange(base_branch, None).AffectedFiles()],
3746 change.RepositoryRoot(), author,
3747 fopen=file, os_path=os.path, glob=glob.glob,
3748 disable_color=options.no_color).run()
3749
3750
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003751def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003752 """Generates a diff command."""
3753 # Generate diff for the current branch's changes.
3754 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
3755 upstream_commit, '--' ]
3756
3757 if args:
3758 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003759 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003760 diff_cmd.append(arg)
3761 else:
3762 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003763
3764 return diff_cmd
3765
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003766def MatchingFileType(file_name, extensions):
3767 """Returns true if the file name ends with one of the given extensions."""
3768 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003769
enne@chromium.org555cfe42014-01-29 18:21:39 +00003770@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003771def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003772 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00003773 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00003774 GN_EXTS = ['.gn', '.gni']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003775 parser.add_option('--full', action='store_true',
3776 help='Reformat the full content of all touched files')
3777 parser.add_option('--dry-run', action='store_true',
3778 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003779 parser.add_option('--python', action='store_true',
3780 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003781 parser.add_option('--diff', action='store_true',
3782 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003783 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003784
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003785 # git diff generates paths against the root of the repository. Change
3786 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003787 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003788 if rel_base_path:
3789 os.chdir(rel_base_path)
3790
digit@chromium.org29e47272013-05-17 17:01:46 +00003791 # Grab the merge-base commit, i.e. the upstream commit of the current
3792 # branch when it was created or the last time it was rebased. This is
3793 # to cover the case where the user may have called "git fetch origin",
3794 # moving the origin branch to a newer commit, but hasn't rebased yet.
3795 upstream_commit = None
3796 cl = Changelist()
3797 upstream_branch = cl.GetUpstreamBranch()
3798 if upstream_branch:
3799 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
3800 upstream_commit = upstream_commit.strip()
3801
3802 if not upstream_commit:
3803 DieWithError('Could not find base commit for this branch. '
3804 'Are you in detached state?')
3805
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003806 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
3807 diff_output = RunGit(changed_files_cmd)
3808 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00003809 # Filter out files deleted by this CL
3810 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003811
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003812 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
3813 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
3814 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00003815 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00003816
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003817 top_dir = os.path.normpath(
3818 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
3819
3820 # Locate the clang-format binary in the checkout
3821 try:
3822 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
3823 except clang_format.NotFoundError, e:
3824 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00003825
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003826 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
3827 # formatted. This is used to block during the presubmit.
3828 return_value = 0
3829
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003830 if clang_diff_files:
3831 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003832 cmd = [clang_format_tool]
3833 if not opts.dry_run and not opts.diff:
3834 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003835 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003836 if opts.diff:
3837 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003838 else:
3839 env = os.environ.copy()
3840 env['PATH'] = str(os.path.dirname(clang_format_tool))
3841 try:
3842 script = clang_format.FindClangFormatScriptInChromiumTree(
3843 'clang-format-diff.py')
3844 except clang_format.NotFoundError, e:
3845 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003846
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003847 cmd = [sys.executable, script, '-p0']
3848 if not opts.dry_run and not opts.diff:
3849 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003850
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003851 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
3852 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003853
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003854 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
3855 if opts.diff:
3856 sys.stdout.write(stdout)
3857 if opts.dry_run and len(stdout) > 0:
3858 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003859
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003860 # Similar code to above, but using yapf on .py files rather than clang-format
3861 # on C/C++ files
3862 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003863 yapf_tool = gclient_utils.FindExecutable('yapf')
3864 if yapf_tool is None:
3865 DieWithError('yapf not found in PATH')
3866
3867 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003868 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003869 cmd = [yapf_tool]
3870 if not opts.dry_run and not opts.diff:
3871 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003872 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003873 if opts.diff:
3874 sys.stdout.write(stdout)
3875 else:
3876 # TODO(sbc): yapf --lines mode still has some issues.
3877 # https://github.com/google/yapf/issues/154
3878 DieWithError('--python currently only works with --full')
3879
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003880 # Dart's formatter does not have the nice property of only operating on
3881 # modified chunks, so hard code full.
3882 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003883 try:
3884 command = [dart_format.FindDartFmtToolInChromiumTree()]
3885 if not opts.dry_run and not opts.diff:
3886 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003887 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003888
ppi@chromium.org6593d932016-03-03 15:41:15 +00003889 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003890 if opts.dry_run and stdout:
3891 return_value = 2
3892 except dart_format.NotFoundError as e:
erikcorry@chromium.org3e445022015-12-17 09:07:26 +00003893 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
3894 'found in this checkout. Files in other languages are still ' +
3895 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003896
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00003897 # Format GN build files. Always run on full build files for canonical form.
3898 if gn_diff_files:
3899 cmd = ['gn', 'format']
3900 if not opts.dry_run and not opts.diff:
3901 cmd.append('--in-place')
3902 for gn_diff_file in gn_diff_files:
3903 stdout = RunCommand(cmd + [gn_diff_file], cwd=top_dir)
3904 if opts.diff:
3905 sys.stdout.write(stdout)
3906
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003907 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003908
3909
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00003910@subcommand.usage('<codereview url or issue id>')
3911def CMDcheckout(parser, args):
3912 """Checks out a branch associated with a given Rietveld issue."""
3913 _, args = parser.parse_args(args)
3914
3915 if len(args) != 1:
3916 parser.print_help()
3917 return 1
3918
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003919 target_issue = ParseIssueNum(args[0])
3920 if target_issue == None:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00003921 parser.print_help()
3922 return 1
3923
3924 key_and_issues = [x.split() for x in RunGit(
3925 ['config', '--local', '--get-regexp', r'branch\..*\.rietveldissue'])
3926 .splitlines()]
3927 branches = []
3928 for key, issue in key_and_issues:
3929 if issue == target_issue:
3930 branches.append(re.sub(r'branch\.(.*)\.rietveldissue', r'\1', key))
3931
3932 if len(branches) == 0:
3933 print 'No branch found for issue %s.' % target_issue
3934 return 1
3935 if len(branches) == 1:
3936 RunGit(['checkout', branches[0]])
3937 else:
3938 print 'Multiple branches match issue %s:' % target_issue
3939 for i in range(len(branches)):
3940 print '%d: %s' % (i, branches[i])
3941 which = raw_input('Choose by index: ')
3942 try:
3943 RunGit(['checkout', branches[int(which)]])
3944 except (IndexError, ValueError):
3945 print 'Invalid selection, not checking out any branch.'
3946 return 1
3947
3948 return 0
3949
3950
maruel@chromium.org29404b52014-09-08 22:58:00 +00003951def CMDlol(parser, args):
3952 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00003953 print zlib.decompress(base64.b64decode(
3954 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
3955 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
3956 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
3957 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00003958 return 0
3959
3960
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003961class OptionParser(optparse.OptionParser):
3962 """Creates the option parse and add --verbose support."""
3963 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003964 optparse.OptionParser.__init__(
3965 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003966 self.add_option(
3967 '-v', '--verbose', action='count', default=0,
3968 help='Use 2 times for more debugging info')
3969
3970 def parse_args(self, args=None, values=None):
3971 options, args = optparse.OptionParser.parse_args(self, args, values)
3972 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
3973 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
3974 return options, args
3975
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003976
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003977def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00003978 if sys.hexversion < 0x02060000:
3979 print >> sys.stderr, (
3980 '\nYour python version %s is unsupported, please upgrade.\n' %
3981 sys.version.split(' ', 1)[0])
3982 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003983
maruel@chromium.orgddd59412011-11-30 14:20:38 +00003984 # Reload settings.
3985 global settings
3986 settings = Settings()
3987
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003988 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003989 dispatcher = subcommand.CommandDispatcher(__name__)
3990 try:
3991 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003992 except auth.AuthenticationError as e:
3993 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003994 except urllib2.HTTPError, e:
3995 if e.code != 500:
3996 raise
3997 DieWithError(
3998 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
3999 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00004000 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004001
4002
4003if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004004 # These affect sys.stdout so do it outside of main() to simplify mocks in
4005 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00004006 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004007 colorama.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00004008 try:
4009 sys.exit(main(sys.argv[1:]))
4010 except KeyboardInterrupt:
4011 sys.stderr.write('interrupted\n')
4012 sys.exit(1)