blob: 37d0523fc40b6e91afafe0b71ecf11463afcc30b [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00008"""A git-command for integrating reviews on Rietveld and Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000010from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000011from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000012import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000013import collections
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000014import glob
sheyang@google.com6ebaf782015-05-12 19:17:54 +000015import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000016import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import logging
18import optparse
19import os
maruel@chromium.org1033efd2013-07-23 23:25:09 +000020import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000022import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000023import sys
bauerb@chromium.org27386dd2015-02-16 10:45:39 +000024import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025import textwrap
sheyang@google.com6ebaf782015-05-12 19:17:54 +000026import time
27import traceback
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000028import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000030import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000031import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000032import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000033import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000034
35try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000036 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000037except ImportError:
38 pass
39
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000040from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000041from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000043import auth
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +000044from luci_hacks import trigger_luci_job as luci_trigger
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000045import clang_format
tandrii@chromium.org71184c02016-01-13 15:18:44 +000046import commit_queue
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000047import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000048import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000049import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000050import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000051import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000052import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000053import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000054import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000055import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000056import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000057import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000058import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000059import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000060import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000061import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000062import watchlists
63
maruel@chromium.org0633fb42013-08-16 20:06:14 +000064__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000065
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000066DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000067POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000069GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
rmistry@google.comc68112d2015-03-03 12:48:06 +000070REFS_THAT_ALIAS_TO_OTHER_REFS = {
71 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
72 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
73}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000074
thestig@chromium.org44202a22014-03-11 19:22:18 +000075# Valid extensions for files we want to lint.
76DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
77DEFAULT_LINT_IGNORE_REGEX = r"$^"
78
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000079# Shortcut since it quickly becomes redundant.
80Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000081
maruel@chromium.orgddd59412011-11-30 14:20:38 +000082# Initialized in main()
83settings = None
84
85
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000086def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000087 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000088 sys.exit(1)
89
90
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000091def GetNoGitPagerEnv():
92 env = os.environ.copy()
93 # 'cat' is a magical git string that disables pagers on all platforms.
94 env['GIT_PAGER'] = 'cat'
95 return env
96
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000097
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000098def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000099 try:
maruel@chromium.org373af802012-05-25 21:07:33 +0000100 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000101 except subprocess2.CalledProcessError as e:
102 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000103 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000104 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000105 'Command "%s" failed.\n%s' % (
106 ' '.join(args), error_message or e.stdout or ''))
107 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000108
109
110def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000111 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000112 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000113
114
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000115def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000116 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000117 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000118 if suppress_stderr:
119 stderr = subprocess2.VOID
120 else:
121 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000122 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000123 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000124 stdout=subprocess2.PIPE,
125 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000126 return code, out[0]
127 except ValueError:
128 # When the subprocess fails, it returns None. That triggers a ValueError
129 # when trying to unpack the return value into (out, code).
130 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000131
132
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000133def RunGitSilent(args):
134 """Returns stdout, suppresses stderr and ingores the return code."""
135 return RunGitWithCode(args, suppress_stderr=True)[1]
136
137
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000138def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000139 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000140 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000141 return (version.startswith(prefix) and
142 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000143
144
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000145def BranchExists(branch):
146 """Return True if specified branch exists."""
147 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
148 suppress_stderr=True)
149 return not code
150
151
maruel@chromium.org90541732011-04-01 17:54:18 +0000152def ask_for_data(prompt):
153 try:
154 return raw_input(prompt)
155 except KeyboardInterrupt:
156 # Hide the exception.
157 sys.exit(1)
158
159
iannucci@chromium.org79540052012-10-19 23:15:26 +0000160def git_set_branch_value(key, value):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000161 branch = GetCurrentBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000162 if not branch:
163 return
164
165 cmd = ['config']
166 if isinstance(value, int):
167 cmd.append('--int')
168 git_key = 'branch.%s.%s' % (branch, key)
169 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000170
171
172def git_get_branch_default(key, default):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000173 branch = GetCurrentBranch()
iannucci@chromium.org79540052012-10-19 23:15:26 +0000174 if branch:
175 git_key = 'branch.%s.%s' % (branch, key)
176 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
177 try:
178 return int(stdout.strip())
179 except ValueError:
180 pass
181 return default
182
183
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000184def add_git_similarity(parser):
185 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000186 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000187 help='Sets the percentage that a pair of files need to match in order to'
188 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000189 parser.add_option(
190 '--find-copies', action='store_true',
191 help='Allows git to look for copies.')
192 parser.add_option(
193 '--no-find-copies', action='store_false', dest='find_copies',
194 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000195
196 old_parser_args = parser.parse_args
197 def Parse(args):
198 options, args = old_parser_args(args)
199
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000200 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000201 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000202 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000203 print('Note: Saving similarity of %d%% in git config.'
204 % options.similarity)
205 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000206
iannucci@chromium.org79540052012-10-19 23:15:26 +0000207 options.similarity = max(0, min(options.similarity, 100))
208
209 if options.find_copies is None:
210 options.find_copies = bool(
211 git_get_branch_default('git-find-copies', True))
212 else:
213 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000214
215 print('Using %d%% similarity for rename/copy detection. '
216 'Override with --similarity.' % options.similarity)
217
218 return options, args
219 parser.parse_args = Parse
220
221
machenbach@chromium.org45453142015-09-15 08:45:22 +0000222def _get_properties_from_options(options):
223 properties = dict(x.split('=', 1) for x in options.properties)
224 for key, val in properties.iteritems():
225 try:
226 properties[key] = json.loads(val)
227 except ValueError:
228 pass # If a value couldn't be evaluated, treat it as a string.
229 return properties
230
231
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000232def _prefix_master(master):
233 """Convert user-specified master name to full master name.
234
235 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
236 name, while the developers always use shortened master name
237 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
238 function does the conversion for buildbucket migration.
239 """
240 prefix = 'master.'
241 if master.startswith(prefix):
242 return master
243 return '%s%s' % (prefix, master)
244
245
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000246def _buildbucket_retry(operation_name, http, *args, **kwargs):
247 """Retries requests to buildbucket service and returns parsed json content."""
248 try_count = 0
249 while True:
250 response, content = http.request(*args, **kwargs)
251 try:
252 content_json = json.loads(content)
253 except ValueError:
254 content_json = None
255
256 # Buildbucket could return an error even if status==200.
257 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000258 error = content_json.get('error')
259 if error.get('code') == 403:
260 raise BuildbucketResponseException(
261 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000262 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000263 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000264 raise BuildbucketResponseException(msg)
265
266 if response.status == 200:
267 if not content_json:
268 raise BuildbucketResponseException(
269 'Buildbucket returns invalid json content: %s.\n'
270 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
271 content)
272 return content_json
273 if response.status < 500 or try_count >= 2:
274 raise httplib2.HttpLib2Error(content)
275
276 # status >= 500 means transient failures.
277 logging.debug('Transient errors when %s. Will retry.', operation_name)
278 time.sleep(0.5 + 1.5*try_count)
279 try_count += 1
280 assert False, 'unreachable'
281
282
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000283def trigger_luci_job(changelist, masters, options):
284 """Send a job to run on LUCI."""
285 issue_props = changelist.GetIssueProperties()
286 issue = changelist.GetIssue()
287 patchset = changelist.GetMostRecentPatchset()
288 for builders_and_tests in sorted(masters.itervalues()):
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000289 # TODO(hinoka et al): add support for other properties.
290 # Currently, this completely ignores testfilter and other properties.
291 for builder in sorted(builders_and_tests):
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000292 luci_trigger.trigger(
293 builder, 'HEAD', issue, patchset, issue_props['project'])
294
295
machenbach@chromium.org45453142015-09-15 08:45:22 +0000296def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000297 rietveld_url = settings.GetDefaultServerUrl()
298 rietveld_host = urlparse.urlparse(rietveld_url).hostname
299 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
300 http = authenticator.authorize(httplib2.Http())
301 http.force_exception_to_status_code = True
302 issue_props = changelist.GetIssueProperties()
303 issue = changelist.GetIssue()
304 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000305 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000306
307 buildbucket_put_url = (
308 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000309 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000310 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
311 hostname=rietveld_host,
312 issue=issue,
313 patch=patchset)
314
315 batch_req_body = {'builds': []}
316 print_text = []
317 print_text.append('Tried jobs on:')
318 for master, builders_and_tests in sorted(masters.iteritems()):
319 print_text.append('Master: %s' % master)
320 bucket = _prefix_master(master)
321 for builder, tests in sorted(builders_and_tests.iteritems()):
322 print_text.append(' %s: %s' % (builder, tests))
323 parameters = {
324 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000325 'changes': [{
326 'author': {'email': issue_props['owner_email']},
327 'revision': options.revision,
328 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000329 'properties': {
330 'category': category,
331 'issue': issue,
332 'master': master,
333 'patch_project': issue_props['project'],
334 'patch_storage': 'rietveld',
335 'patchset': patchset,
336 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000337 'rietveld': rietveld_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000338 },
339 }
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000340 if tests:
341 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000342 if properties:
343 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000344 if options.clobber:
345 parameters['properties']['clobber'] = True
346 batch_req_body['builds'].append(
347 {
348 'bucket': bucket,
349 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000350 'client_operation_id': str(uuid.uuid4()),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000351 'tags': ['builder:%s' % builder,
352 'buildset:%s' % buildset,
353 'master:%s' % master,
354 'user_agent:git_cl_try']
355 }
356 )
357
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000358 _buildbucket_retry(
359 'triggering tryjobs',
360 http,
361 buildbucket_put_url,
362 'PUT',
363 body=json.dumps(batch_req_body),
364 headers={'Content-Type': 'application/json'}
365 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000366 print_text.append('To see results here, run: git cl try-results')
367 print_text.append('To see results in browser, run: git cl web')
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000368 print '\n'.join(print_text)
kjellander@chromium.org44424542015-06-02 18:35:29 +0000369
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000370
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000371def fetch_try_jobs(auth_config, changelist, options):
372 """Fetches tryjobs from buildbucket.
373
374 Returns a map from build id to build info as json dictionary.
375 """
376 rietveld_url = settings.GetDefaultServerUrl()
377 rietveld_host = urlparse.urlparse(rietveld_url).hostname
378 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
379 if authenticator.has_cached_credentials():
380 http = authenticator.authorize(httplib2.Http())
381 else:
382 print ('Warning: Some results might be missing because %s' %
383 # Get the message on how to login.
384 auth.LoginRequiredError(rietveld_host).message)
385 http = httplib2.Http()
386
387 http.force_exception_to_status_code = True
388
389 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
390 hostname=rietveld_host,
391 issue=changelist.GetIssue(),
392 patch=options.patchset)
393 params = {'tag': 'buildset:%s' % buildset}
394
395 builds = {}
396 while True:
397 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
398 hostname=options.buildbucket_host,
399 params=urllib.urlencode(params))
400 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
401 for build in content.get('builds', []):
402 builds[build['id']] = build
403 if 'next_cursor' in content:
404 params['start_cursor'] = content['next_cursor']
405 else:
406 break
407 return builds
408
409
410def print_tryjobs(options, builds):
411 """Prints nicely result of fetch_try_jobs."""
412 if not builds:
413 print 'No tryjobs scheduled'
414 return
415
416 # Make a copy, because we'll be modifying builds dictionary.
417 builds = builds.copy()
418 builder_names_cache = {}
419
420 def get_builder(b):
421 try:
422 return builder_names_cache[b['id']]
423 except KeyError:
424 try:
425 parameters = json.loads(b['parameters_json'])
426 name = parameters['builder_name']
427 except (ValueError, KeyError) as error:
428 print 'WARNING: failed to get builder name for build %s: %s' % (
429 b['id'], error)
430 name = None
431 builder_names_cache[b['id']] = name
432 return name
433
434 def get_bucket(b):
435 bucket = b['bucket']
436 if bucket.startswith('master.'):
437 return bucket[len('master.'):]
438 return bucket
439
440 if options.print_master:
441 name_fmt = '%%-%ds %%-%ds' % (
442 max(len(str(get_bucket(b))) for b in builds.itervalues()),
443 max(len(str(get_builder(b))) for b in builds.itervalues()))
444 def get_name(b):
445 return name_fmt % (get_bucket(b), get_builder(b))
446 else:
447 name_fmt = '%%-%ds' % (
448 max(len(str(get_builder(b))) for b in builds.itervalues()))
449 def get_name(b):
450 return name_fmt % get_builder(b)
451
452 def sort_key(b):
453 return b['status'], b.get('result'), get_name(b), b.get('url')
454
455 def pop(title, f, color=None, **kwargs):
456 """Pop matching builds from `builds` dict and print them."""
457
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000458 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000459 colorize = str
460 else:
461 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
462
463 result = []
464 for b in builds.values():
465 if all(b.get(k) == v for k, v in kwargs.iteritems()):
466 builds.pop(b['id'])
467 result.append(b)
468 if result:
469 print colorize(title)
470 for b in sorted(result, key=sort_key):
471 print ' ', colorize('\t'.join(map(str, f(b))))
472
473 total = len(builds)
474 pop(status='COMPLETED', result='SUCCESS',
475 title='Successes:', color=Fore.GREEN,
476 f=lambda b: (get_name(b), b.get('url')))
477 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
478 title='Infra Failures:', color=Fore.MAGENTA,
479 f=lambda b: (get_name(b), b.get('url')))
480 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
481 title='Failures:', color=Fore.RED,
482 f=lambda b: (get_name(b), b.get('url')))
483 pop(status='COMPLETED', result='CANCELED',
484 title='Canceled:', color=Fore.MAGENTA,
485 f=lambda b: (get_name(b),))
486 pop(status='COMPLETED', result='FAILURE',
487 failure_reason='INVALID_BUILD_DEFINITION',
488 title='Wrong master/builder name:', color=Fore.MAGENTA,
489 f=lambda b: (get_name(b),))
490 pop(status='COMPLETED', result='FAILURE',
491 title='Other failures:',
492 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
493 pop(status='COMPLETED',
494 title='Other finished:',
495 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
496 pop(status='STARTED',
497 title='Started:', color=Fore.YELLOW,
498 f=lambda b: (get_name(b), b.get('url')))
499 pop(status='SCHEDULED',
500 title='Scheduled:',
501 f=lambda b: (get_name(b), 'id=%s' % b['id']))
502 # The last section is just in case buildbucket API changes OR there is a bug.
503 pop(title='Other:',
504 f=lambda b: (get_name(b), 'id=%s' % b['id']))
505 assert len(builds) == 0
506 print 'Total: %d tryjobs' % total
507
508
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000509def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
510 """Return the corresponding git ref if |base_url| together with |glob_spec|
511 matches the full |url|.
512
513 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
514 """
515 fetch_suburl, as_ref = glob_spec.split(':')
516 if allow_wildcards:
517 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
518 if glob_match:
519 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
520 # "branches/{472,597,648}/src:refs/remotes/svn/*".
521 branch_re = re.escape(base_url)
522 if glob_match.group(1):
523 branch_re += '/' + re.escape(glob_match.group(1))
524 wildcard = glob_match.group(2)
525 if wildcard == '*':
526 branch_re += '([^/]*)'
527 else:
528 # Escape and replace surrounding braces with parentheses and commas
529 # with pipe symbols.
530 wildcard = re.escape(wildcard)
531 wildcard = re.sub('^\\\\{', '(', wildcard)
532 wildcard = re.sub('\\\\,', '|', wildcard)
533 wildcard = re.sub('\\\\}$', ')', wildcard)
534 branch_re += wildcard
535 if glob_match.group(3):
536 branch_re += re.escape(glob_match.group(3))
537 match = re.match(branch_re, url)
538 if match:
539 return re.sub('\*$', match.group(1), as_ref)
540
541 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
542 if fetch_suburl:
543 full_url = base_url + '/' + fetch_suburl
544 else:
545 full_url = base_url
546 if full_url == url:
547 return as_ref
548 return None
549
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000550
iannucci@chromium.org79540052012-10-19 23:15:26 +0000551def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000552 """Prints statistics about the change to the user."""
553 # --no-ext-diff is broken in some versions of Git, so try to work around
554 # this by overriding the environment (but there is still a problem if the
555 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000556 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000557 if 'GIT_EXTERNAL_DIFF' in env:
558 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000559
560 if find_copies:
561 similarity_options = ['--find-copies-harder', '-l100000',
562 '-C%s' % similarity]
563 else:
564 similarity_options = ['-M%s' % similarity]
565
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000566 try:
567 stdout = sys.stdout.fileno()
568 except AttributeError:
569 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000570 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000571 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000572 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000573 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000574
575
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000576class BuildbucketResponseException(Exception):
577 pass
578
579
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000580class Settings(object):
581 def __init__(self):
582 self.default_server = None
583 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000584 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000585 self.is_git_svn = None
586 self.svn_branch = None
587 self.tree_status_url = None
588 self.viewvc_url = None
589 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000590 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000591 self.squash_gerrit_uploads = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000592 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000593 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000594 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000595 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000596
597 def LazyUpdateIfNeeded(self):
598 """Updates the settings from a codereview.settings file, if available."""
599 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000600 # The only value that actually changes the behavior is
601 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000602 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000603 error_ok=True
604 ).strip().lower()
605
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000606 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000607 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000608 LoadCodereviewSettingsFromFile(cr_settings_file)
609 self.updated = True
610
611 def GetDefaultServerUrl(self, error_ok=False):
612 if not self.default_server:
613 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000614 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000615 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000616 if error_ok:
617 return self.default_server
618 if not self.default_server:
619 error_message = ('Could not find settings file. You must configure '
620 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000621 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000622 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000623 return self.default_server
624
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000625 @staticmethod
626 def GetRelativeRoot():
627 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000628
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000629 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000630 if self.root is None:
631 self.root = os.path.abspath(self.GetRelativeRoot())
632 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000633
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000634 def GetGitMirror(self, remote='origin'):
635 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000636 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000637 if not os.path.isdir(local_url):
638 return None
639 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
640 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
641 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
642 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
643 if mirror.exists():
644 return mirror
645 return None
646
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000647 def GetIsGitSvn(self):
648 """Return true if this repo looks like it's using git-svn."""
649 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000650 if self.GetPendingRefPrefix():
651 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
652 self.is_git_svn = False
653 else:
654 # If you have any "svn-remote.*" config keys, we think you're using svn.
655 self.is_git_svn = RunGitWithCode(
656 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000657 return self.is_git_svn
658
659 def GetSVNBranch(self):
660 if self.svn_branch is None:
661 if not self.GetIsGitSvn():
662 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
663
664 # Try to figure out which remote branch we're based on.
665 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000666 # 1) iterate through our branch history and find the svn URL.
667 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000668
669 # regexp matching the git-svn line that contains the URL.
670 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
671
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000672 # We don't want to go through all of history, so read a line from the
673 # pipe at a time.
674 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000675 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000676 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
677 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000678 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000679 for line in proc.stdout:
680 match = git_svn_re.match(line)
681 if match:
682 url = match.group(1)
683 proc.stdout.close() # Cut pipe.
684 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000685
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000686 if url:
687 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
688 remotes = RunGit(['config', '--get-regexp',
689 r'^svn-remote\..*\.url']).splitlines()
690 for remote in remotes:
691 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000692 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000693 remote = match.group(1)
694 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000695 rewrite_root = RunGit(
696 ['config', 'svn-remote.%s.rewriteRoot' % remote],
697 error_ok=True).strip()
698 if rewrite_root:
699 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000700 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000701 ['config', 'svn-remote.%s.fetch' % remote],
702 error_ok=True).strip()
703 if fetch_spec:
704 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
705 if self.svn_branch:
706 break
707 branch_spec = RunGit(
708 ['config', 'svn-remote.%s.branches' % remote],
709 error_ok=True).strip()
710 if branch_spec:
711 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
712 if self.svn_branch:
713 break
714 tag_spec = RunGit(
715 ['config', 'svn-remote.%s.tags' % remote],
716 error_ok=True).strip()
717 if tag_spec:
718 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
719 if self.svn_branch:
720 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000721
722 if not self.svn_branch:
723 DieWithError('Can\'t guess svn branch -- try specifying it on the '
724 'command line')
725
726 return self.svn_branch
727
728 def GetTreeStatusUrl(self, error_ok=False):
729 if not self.tree_status_url:
730 error_message = ('You must configure your tree status URL by running '
731 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000732 self.tree_status_url = self._GetRietveldConfig(
733 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000734 return self.tree_status_url
735
736 def GetViewVCUrl(self):
737 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000738 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000739 return self.viewvc_url
740
rmistry@google.com90752582014-01-14 21:04:50 +0000741 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000742 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000743
rmistry@google.com78948ed2015-07-08 23:09:57 +0000744 def GetIsSkipDependencyUpload(self, branch_name):
745 """Returns true if specified branch should skip dep uploads."""
746 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
747 error_ok=True)
748
rmistry@google.com5626a922015-02-26 14:03:30 +0000749 def GetRunPostUploadHook(self):
750 run_post_upload_hook = self._GetRietveldConfig(
751 'run-post-upload-hook', error_ok=True)
752 return run_post_upload_hook == "True"
753
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000754 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000755 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000756
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000757 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000758 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000759
ukai@chromium.orge8077812012-02-03 03:41:46 +0000760 def GetIsGerrit(self):
761 """Return true if this repo is assosiated with gerrit code review system."""
762 if self.is_gerrit is None:
763 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
764 return self.is_gerrit
765
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000766 def GetSquashGerritUploads(self):
767 """Return true if uploads to Gerrit should be squashed by default."""
768 if self.squash_gerrit_uploads is None:
769 self.squash_gerrit_uploads = (
770 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
771 error_ok=True).strip() == 'true')
772 return self.squash_gerrit_uploads
773
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000774 def GetGitEditor(self):
775 """Return the editor specified in the git config, or None if none is."""
776 if self.git_editor is None:
777 self.git_editor = self._GetConfig('core.editor', error_ok=True)
778 return self.git_editor or None
779
thestig@chromium.org44202a22014-03-11 19:22:18 +0000780 def GetLintRegex(self):
781 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
782 DEFAULT_LINT_REGEX)
783
784 def GetLintIgnoreRegex(self):
785 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
786 DEFAULT_LINT_IGNORE_REGEX)
787
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000788 def GetProject(self):
789 if not self.project:
790 self.project = self._GetRietveldConfig('project', error_ok=True)
791 return self.project
792
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000793 def GetForceHttpsCommitUrl(self):
794 if not self.force_https_commit_url:
795 self.force_https_commit_url = self._GetRietveldConfig(
796 'force-https-commit-url', error_ok=True)
797 return self.force_https_commit_url
798
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000799 def GetPendingRefPrefix(self):
800 if not self.pending_ref_prefix:
801 self.pending_ref_prefix = self._GetRietveldConfig(
802 'pending-ref-prefix', error_ok=True)
803 return self.pending_ref_prefix
804
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000805 def _GetRietveldConfig(self, param, **kwargs):
806 return self._GetConfig('rietveld.' + param, **kwargs)
807
rmistry@google.com78948ed2015-07-08 23:09:57 +0000808 def _GetBranchConfig(self, branch_name, param, **kwargs):
809 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
810
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000811 def _GetConfig(self, param, **kwargs):
812 self.LazyUpdateIfNeeded()
813 return RunGit(['config', param], **kwargs).strip()
814
815
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000816def ShortBranchName(branch):
817 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000818 return branch.replace('refs/heads/', '', 1)
819
820
821def GetCurrentBranchRef():
822 """Returns branch ref (e.g., refs/heads/master) or None."""
823 return RunGit(['symbolic-ref', 'HEAD'],
824 stderr=subprocess2.VOID, error_ok=True).strip() or None
825
826
827def GetCurrentBranch():
828 """Returns current branch or None.
829
830 For refs/heads/* branches, returns just last part. For others, full ref.
831 """
832 branchref = GetCurrentBranchRef()
833 if branchref:
834 return ShortBranchName(branchref)
835 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000836
837
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000838class _ParsedIssueNumberArgument(object):
839 def __init__(self, issue=None, patchset=None, hostname=None):
840 self.issue = issue
841 self.patchset = patchset
842 self.hostname = hostname
843
844 @property
845 def valid(self):
846 return self.issue is not None
847
848
849class _RietveldParsedIssueNumberArgument(_ParsedIssueNumberArgument):
850 def __init__(self, *args, **kwargs):
851 self.patch_url = kwargs.pop('patch_url', None)
852 super(_RietveldParsedIssueNumberArgument, self).__init__(*args, **kwargs)
853
854
855def ParseIssueNumberArgument(arg):
856 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
857 fail_result = _ParsedIssueNumberArgument()
858
859 if arg.isdigit():
860 return _ParsedIssueNumberArgument(issue=int(arg))
861 if not arg.startswith('http'):
862 return fail_result
863 url = gclient_utils.UpgradeToHttps(arg)
864 try:
865 parsed_url = urlparse.urlparse(url)
866 except ValueError:
867 return fail_result
868 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
869 tmp = cls.ParseIssueURL(parsed_url)
870 if tmp is not None:
871 return tmp
872 return fail_result
873
874
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000875class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000876 """Changelist works with one changelist in local branch.
877
878 Supports two codereview backends: Rietveld or Gerrit, selected at object
879 creation.
880
881 Not safe for concurrent multi-{thread,process} use.
882 """
883
884 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
885 """Create a new ChangeList instance.
886
887 If issue is given, the codereview must be given too.
888
889 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
890 Otherwise, it's decided based on current configuration of the local branch,
891 with default being 'rietveld' for backwards compatibility.
892 See _load_codereview_impl for more details.
893
894 **kwargs will be passed directly to codereview implementation.
895 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000896 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000897 global settings
898 if not settings:
899 # Happens when git_cl.py is used as a utility library.
900 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000901
902 if issue:
903 assert codereview, 'codereview must be known, if issue is known'
904
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000905 self.branchref = branchref
906 if self.branchref:
907 self.branch = ShortBranchName(self.branchref)
908 else:
909 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000910 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000911 self.lookedup_issue = False
912 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000913 self.has_description = False
914 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000915 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000916 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000917 self.cc = None
918 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000919 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000920
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000921 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000922 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000923 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000924 assert self._codereview_impl
925 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000926
927 def _load_codereview_impl(self, codereview=None, **kwargs):
928 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000929 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
930 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
931 self._codereview = codereview
932 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000933 return
934
935 # Automatic selection based on issue number set for a current branch.
936 # Rietveld takes precedence over Gerrit.
937 assert not self.issue
938 # Whether we find issue or not, we are doing the lookup.
939 self.lookedup_issue = True
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000940 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000941 setting = cls.IssueSetting(self.GetBranch())
942 issue = RunGit(['config', setting], error_ok=True).strip()
943 if issue:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000944 self._codereview = codereview
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000945 self._codereview_impl = cls(self, **kwargs)
946 self.issue = int(issue)
947 return
948
949 # No issue is set for this branch, so decide based on repo-wide settings.
950 return self._load_codereview_impl(
951 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
952 **kwargs)
953
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000954 def IsGerrit(self):
955 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000956
957 def GetCCList(self):
958 """Return the users cc'd on this CL.
959
960 Return is a string suitable for passing to gcl with the --cc flag.
961 """
962 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000963 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000964 more_cc = ','.join(self.watchers)
965 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
966 return self.cc
967
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000968 def GetCCListWithoutDefault(self):
969 """Return the users cc'd on this CL excluding default ones."""
970 if self.cc is None:
971 self.cc = ','.join(self.watchers)
972 return self.cc
973
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000974 def SetWatchers(self, watchers):
975 """Set the list of email addresses that should be cc'd based on the changed
976 files in this CL.
977 """
978 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000979
980 def GetBranch(self):
981 """Returns the short branch name, e.g. 'master'."""
982 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000983 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000984 if not branchref:
985 return None
986 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000987 self.branch = ShortBranchName(self.branchref)
988 return self.branch
989
990 def GetBranchRef(self):
991 """Returns the full branch name, e.g. 'refs/heads/master'."""
992 self.GetBranch() # Poke the lazy loader.
993 return self.branchref
994
tandrii@chromium.org534f67a2016-04-07 18:47:05 +0000995 def ClearBranch(self):
996 """Clears cached branch data of this object."""
997 self.branch = self.branchref = None
998
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000999 @staticmethod
1000 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001001 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001002 e.g. 'origin', 'refs/heads/master'
1003 """
1004 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001005 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
1006 error_ok=True).strip()
1007 if upstream_branch:
1008 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
1009 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001010 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1011 error_ok=True).strip()
1012 if upstream_branch:
1013 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001014 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001015 # Fall back on trying a git-svn upstream branch.
1016 if settings.GetIsGitSvn():
1017 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001018 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001019 # Else, try to guess the origin remote.
1020 remote_branches = RunGit(['branch', '-r']).split()
1021 if 'origin/master' in remote_branches:
1022 # Fall back on origin/master if it exits.
1023 remote = 'origin'
1024 upstream_branch = 'refs/heads/master'
1025 elif 'origin/trunk' in remote_branches:
1026 # Fall back on origin/trunk if it exists. Generally a shared
1027 # git-svn clone
1028 remote = 'origin'
1029 upstream_branch = 'refs/heads/trunk'
1030 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001031 DieWithError(
1032 'Unable to determine default branch to diff against.\n'
1033 'Either pass complete "git diff"-style arguments, like\n'
1034 ' git cl upload origin/master\n'
1035 'or verify this branch is set up to track another \n'
1036 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001037
1038 return remote, upstream_branch
1039
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001040 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001041 upstream_branch = self.GetUpstreamBranch()
1042 if not BranchExists(upstream_branch):
1043 DieWithError('The upstream for the current branch (%s) does not exist '
1044 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001045 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001046 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001047
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001048 def GetUpstreamBranch(self):
1049 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001050 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001051 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001052 upstream_branch = upstream_branch.replace('refs/heads/',
1053 'refs/remotes/%s/' % remote)
1054 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1055 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001056 self.upstream_branch = upstream_branch
1057 return self.upstream_branch
1058
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001059 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001060 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001061 remote, branch = None, self.GetBranch()
1062 seen_branches = set()
1063 while branch not in seen_branches:
1064 seen_branches.add(branch)
1065 remote, branch = self.FetchUpstreamTuple(branch)
1066 branch = ShortBranchName(branch)
1067 if remote != '.' or branch.startswith('refs/remotes'):
1068 break
1069 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001070 remotes = RunGit(['remote'], error_ok=True).split()
1071 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001072 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001073 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001074 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001075 logging.warning('Could not determine which remote this change is '
1076 'associated with, so defaulting to "%s". This may '
1077 'not be what you want. You may prevent this message '
1078 'by running "git svn info" as documented here: %s',
1079 self._remote,
1080 GIT_INSTRUCTIONS_URL)
1081 else:
1082 logging.warn('Could not determine which remote this change is '
1083 'associated with. You may prevent this message by '
1084 'running "git svn info" as documented here: %s',
1085 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001086 branch = 'HEAD'
1087 if branch.startswith('refs/remotes'):
1088 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001089 elif branch.startswith('refs/branch-heads/'):
1090 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001091 else:
1092 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001093 return self._remote
1094
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001095 def GitSanityChecks(self, upstream_git_obj):
1096 """Checks git repo status and ensures diff is from local commits."""
1097
sbc@chromium.org79706062015-01-14 21:18:12 +00001098 if upstream_git_obj is None:
1099 if self.GetBranch() is None:
1100 print >> sys.stderr, (
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00001101 'ERROR: unable to determine current branch (detached HEAD?)')
sbc@chromium.org79706062015-01-14 21:18:12 +00001102 else:
1103 print >> sys.stderr, (
1104 'ERROR: no upstream branch')
1105 return False
1106
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001107 # Verify the commit we're diffing against is in our current branch.
1108 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1109 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1110 if upstream_sha != common_ancestor:
1111 print >> sys.stderr, (
1112 'ERROR: %s is not in the current branch. You may need to rebase '
1113 'your tracking branch' % upstream_sha)
1114 return False
1115
1116 # List the commits inside the diff, and verify they are all local.
1117 commits_in_diff = RunGit(
1118 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1119 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1120 remote_branch = remote_branch.strip()
1121 if code != 0:
1122 _, remote_branch = self.GetRemoteBranch()
1123
1124 commits_in_remote = RunGit(
1125 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1126
1127 common_commits = set(commits_in_diff) & set(commits_in_remote)
1128 if common_commits:
1129 print >> sys.stderr, (
1130 'ERROR: Your diff contains %d commits already in %s.\n'
1131 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1132 'the diff. If you are using a custom git flow, you can override'
1133 ' the reference used for this check with "git config '
1134 'gitcl.remotebranch <git-ref>".' % (
1135 len(common_commits), remote_branch, upstream_git_obj))
1136 return False
1137 return True
1138
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001139 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001140 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001141
1142 Returns None if it is not set.
1143 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001144 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
1145 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001146
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001147 def GetGitSvnRemoteUrl(self):
1148 """Return the configured git-svn remote URL parsed from git svn info.
1149
1150 Returns None if it is not set.
1151 """
1152 # URL is dependent on the current directory.
1153 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1154 if data:
1155 keys = dict(line.split(': ', 1) for line in data.splitlines()
1156 if ': ' in line)
1157 return keys.get('URL', None)
1158 return None
1159
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001160 def GetRemoteUrl(self):
1161 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1162
1163 Returns None if there is no remote.
1164 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001165 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001166 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1167
1168 # If URL is pointing to a local directory, it is probably a git cache.
1169 if os.path.isdir(url):
1170 url = RunGit(['config', 'remote.%s.url' % remote],
1171 error_ok=True,
1172 cwd=url).strip()
1173 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001174
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001175 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001176 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001177 if self.issue is None and not self.lookedup_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001178 issue = RunGit(['config',
1179 self._codereview_impl.IssueSetting(self.GetBranch())],
1180 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001181 self.issue = int(issue) or None if issue else None
1182 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001183 return self.issue
1184
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001185 def GetIssueURL(self):
1186 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001187 issue = self.GetIssue()
1188 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001189 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001190 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001191
1192 def GetDescription(self, pretty=False):
1193 if not self.has_description:
1194 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001195 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001196 self.has_description = True
1197 if pretty:
1198 wrapper = textwrap.TextWrapper()
1199 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1200 return wrapper.fill(self.description)
1201 return self.description
1202
1203 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001204 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001205 if self.patchset is None and not self.lookedup_patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001206 patchset = RunGit(['config', self._codereview_impl.PatchsetSetting()],
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001207 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001208 self.patchset = int(patchset) or None if patchset else None
1209 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001210 return self.patchset
1211
1212 def SetPatchset(self, patchset):
1213 """Set this branch's patchset. If patchset=0, clears the patchset."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001214 patchset_setting = self._codereview_impl.PatchsetSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001215 if patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001216 RunGit(['config', patchset_setting, str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001217 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001218 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001219 RunGit(['config', '--unset', patchset_setting],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001220 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001221 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001222
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001223 def SetIssue(self, issue=None):
1224 """Set this branch's issue. If issue isn't given, clears the issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001225 issue_setting = self._codereview_impl.IssueSetting(self.GetBranch())
1226 codereview_setting = self._codereview_impl.GetCodereviewServerSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001227 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001228 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001229 RunGit(['config', issue_setting, str(issue)])
1230 codereview_server = self._codereview_impl.GetCodereviewServer()
1231 if codereview_server:
1232 RunGit(['config', codereview_setting, codereview_server])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001233 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +00001234 current_issue = self.GetIssue()
1235 if current_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001236 RunGit(['config', '--unset', issue_setting])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001237 self.issue = None
1238 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001239
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001240 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001241 if not self.GitSanityChecks(upstream_branch):
1242 DieWithError('\nGit sanity check failure')
1243
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001244 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001245 if not root:
1246 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001247 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001248
1249 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001250 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001251 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001252 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001253 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001254 except subprocess2.CalledProcessError:
1255 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001256 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001257 'This branch probably doesn\'t exist anymore. To reset the\n'
1258 'tracking branch, please run\n'
1259 ' git branch --set-upstream %s trunk\n'
1260 'replacing trunk with origin/master or the relevant branch') %
1261 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001262
maruel@chromium.org52424302012-08-29 15:14:30 +00001263 issue = self.GetIssue()
1264 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001265 if issue:
1266 description = self.GetDescription()
1267 else:
1268 # If the change was never uploaded, use the log messages of all commits
1269 # up to the branch point, as git cl upload will prefill the description
1270 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001271 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1272 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001273
1274 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001275 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001276 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001277 name,
1278 description,
1279 absroot,
1280 files,
1281 issue,
1282 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001283 author,
1284 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001285
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001286 def UpdateDescription(self, description):
1287 self.description = description
1288 return self._codereview_impl.UpdateDescriptionRemote(description)
1289
1290 def RunHook(self, committing, may_prompt, verbose, change):
1291 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1292 try:
1293 return presubmit_support.DoPresubmitChecks(change, committing,
1294 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1295 default_presubmit=None, may_prompt=may_prompt,
1296 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit())
1297 except presubmit_support.PresubmitFailure, e:
1298 DieWithError(
1299 ('%s\nMaybe your depot_tools is out of date?\n'
1300 'If all fails, contact maruel@') % e)
1301
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001302 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1303 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001304 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1305 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001306 else:
1307 # Assume url.
1308 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1309 urlparse.urlparse(issue_arg))
1310 if not parsed_issue_arg or not parsed_issue_arg.valid:
1311 DieWithError('Failed to parse issue argument "%s". '
1312 'Must be an issue number or a valid URL.' % issue_arg)
1313 return self._codereview_impl.CMDPatchWithParsedIssue(
1314 parsed_issue_arg, reject, nocommit, directory)
1315
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001316 def CMDUpload(self, options, git_diff_args, orig_args):
1317 """Uploads a change to codereview."""
1318 if git_diff_args:
1319 # TODO(ukai): is it ok for gerrit case?
1320 base_branch = git_diff_args[0]
1321 else:
1322 if self.GetBranch() is None:
1323 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1324
1325 # Default to diffing against common ancestor of upstream branch
1326 base_branch = self.GetCommonAncestorWithUpstream()
1327 git_diff_args = [base_branch, 'HEAD']
1328
1329 # Make sure authenticated to codereview before running potentially expensive
1330 # hooks. It is a fast, best efforts check. Codereview still can reject the
1331 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001332 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001333
1334 # Apply watchlists on upload.
1335 change = self.GetChange(base_branch, None)
1336 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1337 files = [f.LocalPath() for f in change.AffectedFiles()]
1338 if not options.bypass_watchlists:
1339 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1340
1341 if not options.bypass_hooks:
1342 if options.reviewers or options.tbr_owners:
1343 # Set the reviewer list now so that presubmit checks can access it.
1344 change_description = ChangeDescription(change.FullDescriptionText())
1345 change_description.update_reviewers(options.reviewers,
1346 options.tbr_owners,
1347 change)
1348 change.SetDescriptionText(change_description.description)
1349 hook_results = self.RunHook(committing=False,
1350 may_prompt=not options.force,
1351 verbose=options.verbose,
1352 change=change)
1353 if not hook_results.should_continue():
1354 return 1
1355 if not options.reviewers and hook_results.reviewers:
1356 options.reviewers = hook_results.reviewers.split(',')
1357
1358 if self.GetIssue():
1359 latest_patchset = self.GetMostRecentPatchset()
1360 local_patchset = self.GetPatchset()
1361 if (latest_patchset and local_patchset and
1362 local_patchset != latest_patchset):
1363 print ('The last upload made from this repository was patchset #%d but '
1364 'the most recent patchset on the server is #%d.'
1365 % (local_patchset, latest_patchset))
1366 print ('Uploading will still work, but if you\'ve uploaded to this '
1367 'issue from another machine or branch the patch you\'re '
1368 'uploading now might not include those changes.')
1369 ask_for_data('About to upload; enter to confirm.')
1370
1371 print_stats(options.similarity, options.find_copies, git_diff_args)
1372 ret = self.CMDUploadChange(options, git_diff_args, change)
1373 if not ret:
1374 git_set_branch_value('last-upload-hash',
1375 RunGit(['rev-parse', 'HEAD']).strip())
1376 # Run post upload hooks, if specified.
1377 if settings.GetRunPostUploadHook():
1378 presubmit_support.DoPostUploadExecuter(
1379 change,
1380 self,
1381 settings.GetRoot(),
1382 options.verbose,
1383 sys.stdout)
1384
1385 # Upload all dependencies if specified.
1386 if options.dependencies:
1387 print
1388 print '--dependencies has been specified.'
1389 print 'All dependent local branches will be re-uploaded.'
1390 print
1391 # Remove the dependencies flag from args so that we do not end up in a
1392 # loop.
1393 orig_args.remove('--dependencies')
1394 ret = upload_branch_deps(self, orig_args)
1395 return ret
1396
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001397 # Forward methods to codereview specific implementation.
1398
1399 def CloseIssue(self):
1400 return self._codereview_impl.CloseIssue()
1401
1402 def GetStatus(self):
1403 return self._codereview_impl.GetStatus()
1404
1405 def GetCodereviewServer(self):
1406 return self._codereview_impl.GetCodereviewServer()
1407
1408 def GetApprovingReviewers(self):
1409 return self._codereview_impl.GetApprovingReviewers()
1410
1411 def GetMostRecentPatchset(self):
1412 return self._codereview_impl.GetMostRecentPatchset()
1413
1414 def __getattr__(self, attr):
1415 # This is because lots of untested code accesses Rietveld-specific stuff
1416 # directly, and it's hard to fix for sure. So, just let it work, and fix
1417 # on a cases by case basis.
1418 return getattr(self._codereview_impl, attr)
1419
1420
1421class _ChangelistCodereviewBase(object):
1422 """Abstract base class encapsulating codereview specifics of a changelist."""
1423 def __init__(self, changelist):
1424 self._changelist = changelist # instance of Changelist
1425
1426 def __getattr__(self, attr):
1427 # Forward methods to changelist.
1428 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1429 # _RietveldChangelistImpl to avoid this hack?
1430 return getattr(self._changelist, attr)
1431
1432 def GetStatus(self):
1433 """Apply a rough heuristic to give a simple summary of an issue's review
1434 or CQ status, assuming adherence to a common workflow.
1435
1436 Returns None if no issue for this branch, or specific string keywords.
1437 """
1438 raise NotImplementedError()
1439
1440 def GetCodereviewServer(self):
1441 """Returns server URL without end slash, like "https://codereview.com"."""
1442 raise NotImplementedError()
1443
1444 def FetchDescription(self):
1445 """Fetches and returns description from the codereview server."""
1446 raise NotImplementedError()
1447
1448 def GetCodereviewServerSetting(self):
1449 """Returns git config setting for the codereview server."""
1450 raise NotImplementedError()
1451
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001452 @classmethod
1453 def IssueSetting(cls, branch):
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001454 return 'branch.%s.%s' % (branch, cls.IssueSettingSuffix())
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001455
1456 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001457 def IssueSettingSuffix(cls):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001458 """Returns name of git config setting which stores issue number for a given
1459 branch."""
1460 raise NotImplementedError()
1461
1462 def PatchsetSetting(self):
1463 """Returns name of git config setting which stores issue number."""
1464 raise NotImplementedError()
1465
1466 def GetRieveldObjForPresubmit(self):
1467 # This is an unfortunate Rietveld-embeddedness in presubmit.
1468 # For non-Rietveld codereviews, this probably should return a dummy object.
1469 raise NotImplementedError()
1470
1471 def UpdateDescriptionRemote(self, description):
1472 """Update the description on codereview site."""
1473 raise NotImplementedError()
1474
1475 def CloseIssue(self):
1476 """Closes the issue."""
1477 raise NotImplementedError()
1478
1479 def GetApprovingReviewers(self):
1480 """Returns a list of reviewers approving the change.
1481
1482 Note: not necessarily committers.
1483 """
1484 raise NotImplementedError()
1485
1486 def GetMostRecentPatchset(self):
1487 """Returns the most recent patchset number from the codereview site."""
1488 raise NotImplementedError()
1489
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001490 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1491 directory):
1492 """Fetches and applies the issue.
1493
1494 Arguments:
1495 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1496 reject: if True, reject the failed patch instead of switching to 3-way
1497 merge. Rietveld only.
1498 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1499 only.
1500 directory: switch to directory before applying the patch. Rietveld only.
1501 """
1502 raise NotImplementedError()
1503
1504 @staticmethod
1505 def ParseIssueURL(parsed_url):
1506 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1507 failed."""
1508 raise NotImplementedError()
1509
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001510 def EnsureAuthenticated(self, force):
1511 """Best effort check that user is authenticated with codereview server.
1512
1513 Arguments:
1514 force: whether to skip confirmation questions.
1515 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001516 raise NotImplementedError()
1517
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001518 def CMDUploadChange(self, options, args, change):
1519 """Uploads a change to codereview."""
1520 raise NotImplementedError()
1521
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001522
1523class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1524 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1525 super(_RietveldChangelistImpl, self).__init__(changelist)
1526 assert settings, 'must be initialized in _ChangelistCodereviewBase'
1527 settings.GetDefaultServerUrl()
1528
1529 self._rietveld_server = rietveld_server
1530 self._auth_config = auth_config
1531 self._props = None
1532 self._rpc_server = None
1533
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001534 def GetCodereviewServer(self):
1535 if not self._rietveld_server:
1536 # If we're on a branch then get the server potentially associated
1537 # with that branch.
1538 if self.GetIssue():
1539 rietveld_server_setting = self.GetCodereviewServerSetting()
1540 if rietveld_server_setting:
1541 self._rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1542 ['config', rietveld_server_setting], error_ok=True).strip())
1543 if not self._rietveld_server:
1544 self._rietveld_server = settings.GetDefaultServerUrl()
1545 return self._rietveld_server
1546
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001547 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001548 """Best effort check that user is authenticated with Rietveld server."""
1549 if self._auth_config.use_oauth2:
1550 authenticator = auth.get_authenticator_for_host(
1551 self.GetCodereviewServer(), self._auth_config)
1552 if not authenticator.has_cached_credentials():
1553 raise auth.LoginRequiredError(self.GetCodereviewServer())
1554
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001555 def FetchDescription(self):
1556 issue = self.GetIssue()
1557 assert issue
1558 try:
1559 return self.RpcServer().get_description(issue).strip()
1560 except urllib2.HTTPError as e:
1561 if e.code == 404:
1562 DieWithError(
1563 ('\nWhile fetching the description for issue %d, received a '
1564 '404 (not found)\n'
1565 'error. It is likely that you deleted this '
1566 'issue on the server. If this is the\n'
1567 'case, please run\n\n'
1568 ' git cl issue 0\n\n'
1569 'to clear the association with the deleted issue. Then run '
1570 'this command again.') % issue)
1571 else:
1572 DieWithError(
1573 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1574 except urllib2.URLError as e:
1575 print >> sys.stderr, (
1576 'Warning: Failed to retrieve CL description due to network '
1577 'failure.')
1578 return ''
1579
1580 def GetMostRecentPatchset(self):
1581 return self.GetIssueProperties()['patchsets'][-1]
1582
1583 def GetPatchSetDiff(self, issue, patchset):
1584 return self.RpcServer().get(
1585 '/download/issue%s_%s.diff' % (issue, patchset))
1586
1587 def GetIssueProperties(self):
1588 if self._props is None:
1589 issue = self.GetIssue()
1590 if not issue:
1591 self._props = {}
1592 else:
1593 self._props = self.RpcServer().get_issue_properties(issue, True)
1594 return self._props
1595
1596 def GetApprovingReviewers(self):
1597 return get_approving_reviewers(self.GetIssueProperties())
1598
1599 def AddComment(self, message):
1600 return self.RpcServer().add_comment(self.GetIssue(), message)
1601
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001602 def GetStatus(self):
1603 """Apply a rough heuristic to give a simple summary of an issue's review
1604 or CQ status, assuming adherence to a common workflow.
1605
1606 Returns None if no issue for this branch, or one of the following keywords:
1607 * 'error' - error from review tool (including deleted issues)
1608 * 'unsent' - not sent for review
1609 * 'waiting' - waiting for review
1610 * 'reply' - waiting for owner to reply to review
1611 * 'lgtm' - LGTM from at least one approved reviewer
1612 * 'commit' - in the commit queue
1613 * 'closed' - closed
1614 """
1615 if not self.GetIssue():
1616 return None
1617
1618 try:
1619 props = self.GetIssueProperties()
1620 except urllib2.HTTPError:
1621 return 'error'
1622
1623 if props.get('closed'):
1624 # Issue is closed.
1625 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001626 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001627 # Issue is in the commit queue.
1628 return 'commit'
1629
1630 try:
1631 reviewers = self.GetApprovingReviewers()
1632 except urllib2.HTTPError:
1633 return 'error'
1634
1635 if reviewers:
1636 # Was LGTM'ed.
1637 return 'lgtm'
1638
1639 messages = props.get('messages') or []
1640
1641 if not messages:
1642 # No message was sent.
1643 return 'unsent'
1644 if messages[-1]['sender'] != props.get('owner_email'):
1645 # Non-LGTM reply from non-owner
1646 return 'reply'
1647 return 'waiting'
1648
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001649 def UpdateDescriptionRemote(self, description):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001650 return self.RpcServer().update_description(
1651 self.GetIssue(), self.description)
1652
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001653 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001654 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001655
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001656 def SetFlag(self, flag, value):
1657 """Patchset must match."""
1658 if not self.GetPatchset():
1659 DieWithError('The patchset needs to match. Send another patchset.')
1660 try:
1661 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001662 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001663 except urllib2.HTTPError, e:
1664 if e.code == 404:
1665 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1666 if e.code == 403:
1667 DieWithError(
1668 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1669 'match?') % (self.GetIssue(), self.GetPatchset()))
1670 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001671
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001672 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001673 """Returns an upload.RpcServer() to access this review's rietveld instance.
1674 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001675 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001676 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001677 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001678 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001679 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001680
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001681 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001682 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001683 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001684
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001685 def PatchsetSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001686 """Return the git setting that stores this change's most recent patchset."""
1687 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1688
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001689 def GetCodereviewServerSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001690 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001691 branch = self.GetBranch()
1692 if branch:
1693 return 'branch.%s.rietveldserver' % branch
1694 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001695
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001696 def GetRieveldObjForPresubmit(self):
1697 return self.RpcServer()
1698
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001699 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1700 directory):
1701 # TODO(maruel): Use apply_issue.py
1702
1703 # PatchIssue should never be called with a dirty tree. It is up to the
1704 # caller to check this, but just in case we assert here since the
1705 # consequences of the caller not checking this could be dire.
1706 assert(not git_common.is_dirty_git_tree('apply'))
1707 assert(parsed_issue_arg.valid)
1708 self._changelist.issue = parsed_issue_arg.issue
1709 if parsed_issue_arg.hostname:
1710 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1711
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001712 if (isinstance(parsed_issue_arg, _RietveldParsedIssueNumberArgument) and
1713 parsed_issue_arg.patch_url):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001714 assert parsed_issue_arg.patchset
1715 patchset = parsed_issue_arg.patchset
1716 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1717 else:
1718 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1719 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1720
1721 # Switch up to the top-level directory, if necessary, in preparation for
1722 # applying the patch.
1723 top = settings.GetRelativeRoot()
1724 if top:
1725 os.chdir(top)
1726
1727 # Git patches have a/ at the beginning of source paths. We strip that out
1728 # with a sed script rather than the -p flag to patch so we can feed either
1729 # Git or svn-style patches into the same apply command.
1730 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1731 try:
1732 patch_data = subprocess2.check_output(
1733 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1734 except subprocess2.CalledProcessError:
1735 DieWithError('Git patch mungling failed.')
1736 logging.info(patch_data)
1737
1738 # We use "git apply" to apply the patch instead of "patch" so that we can
1739 # pick up file adds.
1740 # The --index flag means: also insert into the index (so we catch adds).
1741 cmd = ['git', 'apply', '--index', '-p0']
1742 if directory:
1743 cmd.extend(('--directory', directory))
1744 if reject:
1745 cmd.append('--reject')
1746 elif IsGitVersionAtLeast('1.7.12'):
1747 cmd.append('--3way')
1748 try:
1749 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
1750 stdin=patch_data, stdout=subprocess2.VOID)
1751 except subprocess2.CalledProcessError:
1752 print 'Failed to apply the patch'
1753 return 1
1754
1755 # If we had an issue, commit the current state and register the issue.
1756 if not nocommit:
1757 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
1758 'patch from issue %(i)s at patchset '
1759 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
1760 % {'i': self.GetIssue(), 'p': patchset})])
1761 self.SetIssue(self.GetIssue())
1762 self.SetPatchset(patchset)
1763 print "Committed patch locally."
1764 else:
1765 print "Patch applied to index."
1766 return 0
1767
1768 @staticmethod
1769 def ParseIssueURL(parsed_url):
1770 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1771 return None
1772 # Typical url: https://domain/<issue_number>[/[other]]
1773 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
1774 if match:
1775 return _RietveldParsedIssueNumberArgument(
1776 issue=int(match.group(1)),
1777 hostname=parsed_url.netloc)
1778 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
1779 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
1780 if match:
1781 return _RietveldParsedIssueNumberArgument(
1782 issue=int(match.group(1)),
1783 patchset=int(match.group(2)),
1784 hostname=parsed_url.netloc,
1785 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
1786 return None
1787
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001788 def CMDUploadChange(self, options, args, change):
1789 """Upload the patch to Rietveld."""
1790 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1791 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001792 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
1793 if options.emulate_svn_auto_props:
1794 upload_args.append('--emulate_svn_auto_props')
1795
1796 change_desc = None
1797
1798 if options.email is not None:
1799 upload_args.extend(['--email', options.email])
1800
1801 if self.GetIssue():
1802 if options.title:
1803 upload_args.extend(['--title', options.title])
1804 if options.message:
1805 upload_args.extend(['--message', options.message])
1806 upload_args.extend(['--issue', str(self.GetIssue())])
1807 print ('This branch is associated with issue %s. '
1808 'Adding patch to that issue.' % self.GetIssue())
1809 else:
1810 if options.title:
1811 upload_args.extend(['--title', options.title])
1812 message = (options.title or options.message or
1813 CreateDescriptionFromLog(args))
1814 change_desc = ChangeDescription(message)
1815 if options.reviewers or options.tbr_owners:
1816 change_desc.update_reviewers(options.reviewers,
1817 options.tbr_owners,
1818 change)
1819 if not options.force:
1820 change_desc.prompt()
1821
1822 if not change_desc.description:
1823 print "Description is empty; aborting."
1824 return 1
1825
1826 upload_args.extend(['--message', change_desc.description])
1827 if change_desc.get_reviewers():
1828 upload_args.append('--reviewers=%s' % ','.join(
1829 change_desc.get_reviewers()))
1830 if options.send_mail:
1831 if not change_desc.get_reviewers():
1832 DieWithError("Must specify reviewers to send email.")
1833 upload_args.append('--send_mail')
1834
1835 # We check this before applying rietveld.private assuming that in
1836 # rietveld.cc only addresses which we can send private CLs to are listed
1837 # if rietveld.private is set, and so we should ignore rietveld.cc only
1838 # when --private is specified explicitly on the command line.
1839 if options.private:
1840 logging.warn('rietveld.cc is ignored since private flag is specified. '
1841 'You need to review and add them manually if necessary.')
1842 cc = self.GetCCListWithoutDefault()
1843 else:
1844 cc = self.GetCCList()
1845 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
1846 if cc:
1847 upload_args.extend(['--cc', cc])
1848
1849 if options.private or settings.GetDefaultPrivateFlag() == "True":
1850 upload_args.append('--private')
1851
1852 upload_args.extend(['--git_similarity', str(options.similarity)])
1853 if not options.find_copies:
1854 upload_args.extend(['--git_no_find_copies'])
1855
1856 # Include the upstream repo's URL in the change -- this is useful for
1857 # projects that have their source spread across multiple repos.
1858 remote_url = self.GetGitBaseUrlFromConfig()
1859 if not remote_url:
1860 if settings.GetIsGitSvn():
1861 remote_url = self.GetGitSvnRemoteUrl()
1862 else:
1863 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
1864 remote_url = '%s@%s' % (self.GetRemoteUrl(),
1865 self.GetUpstreamBranch().split('/')[-1])
1866 if remote_url:
1867 upload_args.extend(['--base_url', remote_url])
1868 remote, remote_branch = self.GetRemoteBranch()
1869 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
1870 settings.GetPendingRefPrefix())
1871 if target_ref:
1872 upload_args.extend(['--target_ref', target_ref])
1873
1874 # Look for dependent patchsets. See crbug.com/480453 for more details.
1875 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
1876 upstream_branch = ShortBranchName(upstream_branch)
1877 if remote is '.':
1878 # A local branch is being tracked.
1879 local_branch = ShortBranchName(upstream_branch)
1880 if settings.GetIsSkipDependencyUpload(local_branch):
1881 print
1882 print ('Skipping dependency patchset upload because git config '
1883 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
1884 print
1885 else:
1886 auth_config = auth.extract_auth_config_from_options(options)
1887 branch_cl = Changelist(branchref=local_branch,
1888 auth_config=auth_config)
1889 branch_cl_issue_url = branch_cl.GetIssueURL()
1890 branch_cl_issue = branch_cl.GetIssue()
1891 branch_cl_patchset = branch_cl.GetPatchset()
1892 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
1893 upload_args.extend(
1894 ['--depends_on_patchset', '%s:%s' % (
1895 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001896 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001897 '\n'
1898 'The current branch (%s) is tracking a local branch (%s) with '
1899 'an associated CL.\n'
1900 'Adding %s/#ps%s as a dependency patchset.\n'
1901 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
1902 branch_cl_patchset))
1903
1904 project = settings.GetProject()
1905 if project:
1906 upload_args.extend(['--project', project])
1907
1908 if options.cq_dry_run:
1909 upload_args.extend(['--cq_dry_run'])
1910
1911 try:
1912 upload_args = ['upload'] + upload_args + args
1913 logging.info('upload.RealMain(%s)', upload_args)
1914 issue, patchset = upload.RealMain(upload_args)
1915 issue = int(issue)
1916 patchset = int(patchset)
1917 except KeyboardInterrupt:
1918 sys.exit(1)
1919 except:
1920 # If we got an exception after the user typed a description for their
1921 # change, back up the description before re-raising.
1922 if change_desc:
1923 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1924 print('\nGot exception while uploading -- saving description to %s\n' %
1925 backup_path)
1926 backup_file = open(backup_path, 'w')
1927 backup_file.write(change_desc.description)
1928 backup_file.close()
1929 raise
1930
1931 if not self.GetIssue():
1932 self.SetIssue(issue)
1933 self.SetPatchset(patchset)
1934
1935 if options.use_commit_queue:
1936 self.SetFlag('commit', '1')
1937 return 0
1938
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001939
1940class _GerritChangelistImpl(_ChangelistCodereviewBase):
1941 def __init__(self, changelist, auth_config=None):
1942 # auth_config is Rietveld thing, kept here to preserve interface only.
1943 super(_GerritChangelistImpl, self).__init__(changelist)
1944 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001945 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001946 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001947 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001948
1949 def _GetGerritHost(self):
1950 # Lazy load of configs.
1951 self.GetCodereviewServer()
1952 return self._gerrit_host
1953
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001954 def _GetGitHost(self):
1955 """Returns git host to be used when uploading change to Gerrit."""
1956 return urlparse.urlparse(self.GetRemoteUrl()).netloc
1957
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001958 def GetCodereviewServer(self):
1959 if not self._gerrit_server:
1960 # If we're on a branch then get the server potentially associated
1961 # with that branch.
1962 if self.GetIssue():
1963 gerrit_server_setting = self.GetCodereviewServerSetting()
1964 if gerrit_server_setting:
1965 self._gerrit_server = RunGit(['config', gerrit_server_setting],
1966 error_ok=True).strip()
1967 if self._gerrit_server:
1968 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
1969 if not self._gerrit_server:
1970 # We assume repo to be hosted on Gerrit, and hence Gerrit server
1971 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001972 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001973 parts[0] = parts[0] + '-review'
1974 self._gerrit_host = '.'.join(parts)
1975 self._gerrit_server = 'https://%s' % self._gerrit_host
1976 return self._gerrit_server
1977
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001978 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001979 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001980 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001981
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001982 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001983 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001984 # Lazy-loader to identify Gerrit and Git hosts.
1985 if gerrit_util.GceAuthenticator.is_gce():
1986 return
1987 self.GetCodereviewServer()
1988 git_host = self._GetGitHost()
1989 assert self._gerrit_server and self._gerrit_host
1990 cookie_auth = gerrit_util.CookiesAuthenticator()
1991
1992 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
1993 git_auth = cookie_auth.get_auth_header(git_host)
1994 if gerrit_auth and git_auth:
1995 if gerrit_auth == git_auth:
1996 return
1997 print((
1998 'WARNING: you have different credentials for Gerrit and git hosts.\n'
1999 ' Check your %s or %s file for credentials of hosts:\n'
2000 ' %s\n'
2001 ' %s\n'
2002 ' %s') %
2003 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2004 git_host, self._gerrit_host,
2005 cookie_auth.get_new_password_message(git_host)))
2006 if not force:
2007 ask_for_data('If you know what you are doing, press Enter to continue, '
2008 'Ctrl+C to abort.')
2009 return
2010 else:
2011 missing = (
2012 [] if gerrit_auth else [self._gerrit_host] +
2013 [] if git_auth else [git_host])
2014 DieWithError('Credentials for the following hosts are required:\n'
2015 ' %s\n'
2016 'These are read from %s (or legacy %s)\n'
2017 '%s' % (
2018 '\n '.join(missing),
2019 cookie_auth.get_gitcookies_path(),
2020 cookie_auth.get_netrc_path(),
2021 cookie_auth.get_new_password_message(git_host)))
2022
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002023
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002024 def PatchsetSetting(self):
2025 """Return the git setting that stores this change's most recent patchset."""
2026 return 'branch.%s.gerritpatchset' % self.GetBranch()
2027
2028 def GetCodereviewServerSetting(self):
2029 """Returns the git setting that stores this change's Gerrit server."""
2030 branch = self.GetBranch()
2031 if branch:
2032 return 'branch.%s.gerritserver' % branch
2033 return None
2034
2035 def GetRieveldObjForPresubmit(self):
2036 class ThisIsNotRietveldIssue(object):
2037 def __nonzero__(self):
2038 # This is a hack to make presubmit_support think that rietveld is not
2039 # defined, yet still ensure that calls directly result in a decent
2040 # exception message below.
2041 return False
2042
2043 def __getattr__(self, attr):
2044 print(
2045 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2046 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2047 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2048 'or use Rietveld for codereview.\n'
2049 'See also http://crbug.com/579160.' % attr)
2050 raise NotImplementedError()
2051 return ThisIsNotRietveldIssue()
2052
2053 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002054 """Apply a rough heuristic to give a simple summary of an issue's review
2055 or CQ status, assuming adherence to a common workflow.
2056
2057 Returns None if no issue for this branch, or one of the following keywords:
2058 * 'error' - error from review tool (including deleted issues)
2059 * 'unsent' - no reviewers added
2060 * 'waiting' - waiting for review
2061 * 'reply' - waiting for owner to reply to review
2062 * 'not lgtm' - Code-Review -2 from at least one approved reviewer
2063 * 'lgtm' - Code-Review +2 from at least one approved reviewer
2064 * 'commit' - in the commit queue
2065 * 'closed' - abandoned
2066 """
2067 if not self.GetIssue():
2068 return None
2069
2070 try:
2071 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
2072 except httplib.HTTPException:
2073 return 'error'
2074
2075 if data['status'] == 'ABANDONED':
2076 return 'closed'
2077
2078 cq_label = data['labels'].get('Commit-Queue', {})
2079 if cq_label:
2080 # Vote value is a stringified integer, which we expect from 0 to 2.
2081 vote_value = cq_label.get('value', '0')
2082 vote_text = cq_label.get('values', {}).get(vote_value, '')
2083 if vote_text.lower() == 'commit':
2084 return 'commit'
2085
2086 lgtm_label = data['labels'].get('Code-Review', {})
2087 if lgtm_label:
2088 if 'rejected' in lgtm_label:
2089 return 'not lgtm'
2090 if 'approved' in lgtm_label:
2091 return 'lgtm'
2092
2093 if not data.get('reviewers', {}).get('REVIEWER', []):
2094 return 'unsent'
2095
2096 messages = data.get('messages', [])
2097 if messages:
2098 owner = data['owner'].get('_account_id')
2099 last_message_author = messages[-1].get('author', {}).get('_account_id')
2100 if owner != last_message_author:
2101 # Some reply from non-owner.
2102 return 'reply'
2103
2104 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002105
2106 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002107 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002108 return data['revisions'][data['current_revision']]['_number']
2109
2110 def FetchDescription(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002111 data = self._GetChangeDetail(['COMMIT_FOOTERS', 'CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002112 return data['revisions'][data['current_revision']]['commit_with_footers']
2113
2114 def UpdateDescriptionRemote(self, description):
2115 # TODO(tandrii)
2116 raise NotImplementedError()
2117
2118 def CloseIssue(self):
2119 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2120
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002121 def SubmitIssue(self, wait_for_merge=True):
2122 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2123 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002124
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002125 def _GetChangeDetail(self, options=None, issue=None):
2126 options = options or []
2127 issue = issue or self.GetIssue()
2128 assert issue, 'issue required to query Gerrit'
2129 return gerrit_util.GetChangeDetail(self._GetGerritHost(), options, issue)
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002130
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002131 def CMDLand(self, force, bypass_hooks, verbose):
2132 if git_common.is_dirty_git_tree('land'):
2133 return 1
2134 differs = True
2135 last_upload = RunGit(['config',
2136 'branch.%s.gerritsquashhash' % self.GetBranch()],
2137 error_ok=True).strip()
2138 # Note: git diff outputs nothing if there is no diff.
2139 if not last_upload or RunGit(['diff', last_upload]).strip():
2140 print('WARNING: some changes from local branch haven\'t been uploaded')
2141 else:
2142 detail = self._GetChangeDetail(['CURRENT_REVISION'])
2143 if detail['current_revision'] == last_upload:
2144 differs = False
2145 else:
2146 print('WARNING: local branch contents differ from latest uploaded '
2147 'patchset')
2148 if differs:
2149 if not force:
2150 ask_for_data(
2151 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2152 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2153 elif not bypass_hooks:
2154 hook_results = self.RunHook(
2155 committing=True,
2156 may_prompt=not force,
2157 verbose=verbose,
2158 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2159 if not hook_results.should_continue():
2160 return 1
2161
2162 self.SubmitIssue(wait_for_merge=True)
2163 print('Issue %s has been submitted.' % self.GetIssueURL())
2164 return 0
2165
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002166 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2167 directory):
2168 assert not reject
2169 assert not nocommit
2170 assert not directory
2171 assert parsed_issue_arg.valid
2172
2173 self._changelist.issue = parsed_issue_arg.issue
2174
2175 if parsed_issue_arg.hostname:
2176 self._gerrit_host = parsed_issue_arg.hostname
2177 self._gerrit_server = 'https://%s' % self._gerrit_host
2178
2179 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2180
2181 if not parsed_issue_arg.patchset:
2182 # Use current revision by default.
2183 revision_info = detail['revisions'][detail['current_revision']]
2184 patchset = int(revision_info['_number'])
2185 else:
2186 patchset = parsed_issue_arg.patchset
2187 for revision_info in detail['revisions'].itervalues():
2188 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2189 break
2190 else:
2191 DieWithError('Couldn\'t find patchset %i in issue %i' %
2192 (parsed_issue_arg.patchset, self.GetIssue()))
2193
2194 fetch_info = revision_info['fetch']['http']
2195 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2196 RunGit(['cherry-pick', 'FETCH_HEAD'])
2197 self.SetIssue(self.GetIssue())
2198 self.SetPatchset(patchset)
2199 print('Committed patch for issue %i pathset %i locally' %
2200 (self.GetIssue(), self.GetPatchset()))
2201 return 0
2202
2203 @staticmethod
2204 def ParseIssueURL(parsed_url):
2205 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2206 return None
2207 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2208 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2209 # Short urls like https://domain/<issue_number> can be used, but don't allow
2210 # specifying the patchset (you'd 404), but we allow that here.
2211 if parsed_url.path == '/':
2212 part = parsed_url.fragment
2213 else:
2214 part = parsed_url.path
2215 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2216 if match:
2217 return _ParsedIssueNumberArgument(
2218 issue=int(match.group(2)),
2219 patchset=int(match.group(4)) if match.group(4) else None,
2220 hostname=parsed_url.netloc)
2221 return None
2222
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002223 def CMDUploadChange(self, options, args, change):
2224 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002225 if options.squash and options.no_squash:
2226 DieWithError('Can only use one of --squash or --no-squash')
2227 options.squash = ((settings.GetSquashGerritUploads() or options.squash) and
2228 not options.no_squash)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002229 # We assume the remote called "origin" is the one we want.
2230 # It is probably not worthwhile to support different workflows.
2231 gerrit_remote = 'origin'
2232
2233 remote, remote_branch = self.GetRemoteBranch()
2234 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2235 pending_prefix='')
2236
2237 if options.title:
2238 # TODO(tandrii): it's now supported by Gerrit, implement!
2239 print "\nPatch titles (-t) are not supported in Gerrit. Aborting..."
2240 return 1
2241
2242 if options.squash:
2243 if not self.GetIssue():
2244 # TODO(tandrii): deperecate this after 2016Q2. Backwards compatibility
2245 # with shadow branch, which used to contain change-id for a given
2246 # branch, using which we can fetch actual issue number and set it as the
2247 # property of the branch, which is the new way.
2248 message = RunGitSilent([
2249 'show', '--format=%B', '-s',
2250 'refs/heads/git_cl_uploads/%s' % self.GetBranch()])
2251 if message:
2252 change_ids = git_footers.get_footer_change_id(message.strip())
2253 if change_ids and len(change_ids) == 1:
2254 details = self._GetChangeDetail(issue=change_ids[0])
2255 if details:
2256 print('WARNING: found old upload in branch git_cl_uploads/%s '
2257 'corresponding to issue %s' %
2258 (self.GetBranch(), details['_number']))
2259 self.SetIssue(details['_number'])
2260 if not self.GetIssue():
2261 DieWithError(
2262 '\n' # For readability of the blob below.
2263 'Found old upload in branch git_cl_uploads/%s, '
2264 'but failed to find corresponding Gerrit issue.\n'
2265 'If you know the issue number, set it manually first:\n'
2266 ' git cl issue 123456\n'
2267 'If you intended to upload this CL as new issue, '
2268 'just delete or rename the old upload branch:\n'
2269 ' git rename-branch git_cl_uploads/%s old_upload-%s\n'
2270 'After that, please run git cl upload again.' %
2271 tuple([self.GetBranch()] * 3))
2272 # End of backwards compatability.
2273
2274 if self.GetIssue():
2275 # Try to get the message from a previous upload.
2276 message = self.GetDescription()
2277 if not message:
2278 DieWithError(
2279 'failed to fetch description from current Gerrit issue %d\n'
2280 '%s' % (self.GetIssue(), self.GetIssueURL()))
2281 change_id = self._GetChangeDetail()['change_id']
2282 while True:
2283 footer_change_ids = git_footers.get_footer_change_id(message)
2284 if footer_change_ids == [change_id]:
2285 break
2286 if not footer_change_ids:
2287 message = git_footers.add_footer_change_id(message, change_id)
2288 print('WARNING: appended missing Change-Id to issue description')
2289 continue
2290 # There is already a valid footer but with different or several ids.
2291 # Doing this automatically is non-trivial as we don't want to lose
2292 # existing other footers, yet we want to append just 1 desired
2293 # Change-Id. Thus, just create a new footer, but let user verify the
2294 # new description.
2295 message = '%s\n\nChange-Id: %s' % (message, change_id)
2296 print(
2297 'WARNING: issue %s has Change-Id footer(s):\n'
2298 ' %s\n'
2299 'but issue has Change-Id %s, according to Gerrit.\n'
2300 'Please, check the proposed correction to the description, '
2301 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2302 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2303 change_id))
2304 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2305 if not options.force:
2306 change_desc = ChangeDescription(message)
2307 change_desc.prompt()
2308 message = change_desc.description
2309 if not message:
2310 DieWithError("Description is empty. Aborting...")
2311 # Continue the while loop.
2312 # Sanity check of this code - we should end up with proper message
2313 # footer.
2314 assert [change_id] == git_footers.get_footer_change_id(message)
2315 change_desc = ChangeDescription(message)
2316 else:
2317 change_desc = ChangeDescription(
2318 options.message or CreateDescriptionFromLog(args))
2319 if not options.force:
2320 change_desc.prompt()
2321 if not change_desc.description:
2322 DieWithError("Description is empty. Aborting...")
2323 message = change_desc.description
2324 change_ids = git_footers.get_footer_change_id(message)
2325 if len(change_ids) > 1:
2326 DieWithError('too many Change-Id footers, at most 1 allowed.')
2327 if not change_ids:
2328 # Generate the Change-Id automatically.
2329 message = git_footers.add_footer_change_id(
2330 message, GenerateGerritChangeId(message))
2331 change_desc.set_description(message)
2332 change_ids = git_footers.get_footer_change_id(message)
2333 assert len(change_ids) == 1
2334 change_id = change_ids[0]
2335
2336 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2337 if remote is '.':
2338 # If our upstream branch is local, we base our squashed commit on its
2339 # squashed version.
2340 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2341 # Check the squashed hash of the parent.
2342 parent = RunGit(['config',
2343 'branch.%s.gerritsquashhash' % upstream_branch_name],
2344 error_ok=True).strip()
2345 # Verify that the upstream branch has been uploaded too, otherwise
2346 # Gerrit will create additional CLs when uploading.
2347 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2348 RunGitSilent(['rev-parse', parent + ':'])):
2349 # TODO(tandrii): remove "old depot_tools" part on April 12, 2016.
2350 DieWithError(
2351 'Upload upstream branch %s first.\n'
2352 'Note: maybe you\'ve uploaded it with --no-squash or with an old '
2353 'version of depot_tools. If so, then re-upload it with:\n'
2354 ' git cl upload --squash\n' % upstream_branch_name)
2355 else:
2356 parent = self.GetCommonAncestorWithUpstream()
2357
2358 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2359 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2360 '-m', message]).strip()
2361 else:
2362 change_desc = ChangeDescription(
2363 options.message or CreateDescriptionFromLog(args))
2364 if not change_desc.description:
2365 DieWithError("Description is empty. Aborting...")
2366
2367 if not git_footers.get_footer_change_id(change_desc.description):
2368 DownloadGerritHook(False)
2369 change_desc.set_description(AddChangeIdToCommitMessage(options, args))
2370 ref_to_push = 'HEAD'
2371 parent = '%s/%s' % (gerrit_remote, branch)
2372 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2373
2374 assert change_desc
2375 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2376 ref_to_push)]).splitlines()
2377 if len(commits) > 1:
2378 print('WARNING: This will upload %d commits. Run the following command '
2379 'to see which commits will be uploaded: ' % len(commits))
2380 print('git log %s..%s' % (parent, ref_to_push))
2381 print('You can also use `git squash-branch` to squash these into a '
2382 'single commit.')
2383 ask_for_data('About to upload; enter to confirm.')
2384
2385 if options.reviewers or options.tbr_owners:
2386 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2387 change)
2388
2389 receive_options = []
2390 cc = self.GetCCList().split(',')
2391 if options.cc:
2392 cc.extend(options.cc)
2393 cc = filter(None, cc)
2394 if cc:
2395 receive_options += ['--cc=' + email for email in cc]
2396 if change_desc.get_reviewers():
2397 receive_options.extend(
2398 '--reviewer=' + email for email in change_desc.get_reviewers())
2399
2400 git_command = ['push']
2401 if receive_options:
2402 git_command.append('--receive-pack=git receive-pack %s' %
2403 ' '.join(receive_options))
2404 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
2405 push_stdout = gclient_utils.CheckCallAndFilter(
2406 ['git'] + git_command,
2407 print_stdout=True,
2408 # Flush after every line: useful for seeing progress when running as
2409 # recipe.
2410 filter_fn=lambda _: sys.stdout.flush())
2411
2412 if options.squash:
2413 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2414 change_numbers = [m.group(1)
2415 for m in map(regex.match, push_stdout.splitlines())
2416 if m]
2417 if len(change_numbers) != 1:
2418 DieWithError(
2419 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2420 'Change-Id: %s') % (len(change_numbers), change_id))
2421 self.SetIssue(change_numbers[0])
2422 RunGit(['config', 'branch.%s.gerritsquashhash' % self.GetBranch(),
2423 ref_to_push])
2424 return 0
2425
2426
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002427
2428_CODEREVIEW_IMPLEMENTATIONS = {
2429 'rietveld': _RietveldChangelistImpl,
2430 'gerrit': _GerritChangelistImpl,
2431}
2432
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002433
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002434class ChangeDescription(object):
2435 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002436 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002437 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002438
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002439 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002440 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002441
agable@chromium.org42c20792013-09-12 17:34:49 +00002442 @property # www.logilab.org/ticket/89786
2443 def description(self): # pylint: disable=E0202
2444 return '\n'.join(self._description_lines)
2445
2446 def set_description(self, desc):
2447 if isinstance(desc, basestring):
2448 lines = desc.splitlines()
2449 else:
2450 lines = [line.rstrip() for line in desc]
2451 while lines and not lines[0]:
2452 lines.pop(0)
2453 while lines and not lines[-1]:
2454 lines.pop(-1)
2455 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002456
piman@chromium.org336f9122014-09-04 02:16:55 +00002457 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002458 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002459 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002460 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002461 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002462 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002463
agable@chromium.org42c20792013-09-12 17:34:49 +00002464 # Get the set of R= and TBR= lines and remove them from the desciption.
2465 regexp = re.compile(self.R_LINE)
2466 matches = [regexp.match(line) for line in self._description_lines]
2467 new_desc = [l for i, l in enumerate(self._description_lines)
2468 if not matches[i]]
2469 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002470
agable@chromium.org42c20792013-09-12 17:34:49 +00002471 # Construct new unified R= and TBR= lines.
2472 r_names = []
2473 tbr_names = []
2474 for match in matches:
2475 if not match:
2476 continue
2477 people = cleanup_list([match.group(2).strip()])
2478 if match.group(1) == 'TBR':
2479 tbr_names.extend(people)
2480 else:
2481 r_names.extend(people)
2482 for name in r_names:
2483 if name not in reviewers:
2484 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002485 if add_owners_tbr:
2486 owners_db = owners.Database(change.RepositoryRoot(),
2487 fopen=file, os_path=os.path, glob=glob.glob)
2488 all_reviewers = set(tbr_names + reviewers)
2489 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2490 all_reviewers)
2491 tbr_names.extend(owners_db.reviewers_for(missing_files,
2492 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002493 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2494 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2495
2496 # Put the new lines in the description where the old first R= line was.
2497 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2498 if 0 <= line_loc < len(self._description_lines):
2499 if new_tbr_line:
2500 self._description_lines.insert(line_loc, new_tbr_line)
2501 if new_r_line:
2502 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002503 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002504 if new_r_line:
2505 self.append_footer(new_r_line)
2506 if new_tbr_line:
2507 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002508
2509 def prompt(self):
2510 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002511 self.set_description([
2512 '# Enter a description of the change.',
2513 '# This will be displayed on the codereview site.',
2514 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002515 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002516 '--------------------',
2517 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002518
agable@chromium.org42c20792013-09-12 17:34:49 +00002519 regexp = re.compile(self.BUG_LINE)
2520 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00002521 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00002522 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002523 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002524 if not content:
2525 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002526 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002527
2528 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002529 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2530 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002531 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002532 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002533
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002534 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00002535 if self._description_lines:
2536 # Add an empty line if either the last line or the new line isn't a tag.
2537 last_line = self._description_lines[-1]
2538 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
2539 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2540 self._description_lines.append('')
2541 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002542
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002543 def get_reviewers(self):
2544 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002545 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
2546 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002547 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002548
2549
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002550def get_approving_reviewers(props):
2551 """Retrieves the reviewers that approved a CL from the issue properties with
2552 messages.
2553
2554 Note that the list may contain reviewers that are not committer, thus are not
2555 considered by the CQ.
2556 """
2557 return sorted(
2558 set(
2559 message['sender']
2560 for message in props['messages']
2561 if message['approval'] and message['sender'] in props['reviewers']
2562 )
2563 )
2564
2565
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002566def FindCodereviewSettingsFile(filename='codereview.settings'):
2567 """Finds the given file starting in the cwd and going up.
2568
2569 Only looks up to the top of the repository unless an
2570 'inherit-review-settings-ok' file exists in the root of the repository.
2571 """
2572 inherit_ok_file = 'inherit-review-settings-ok'
2573 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002574 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002575 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2576 root = '/'
2577 while True:
2578 if filename in os.listdir(cwd):
2579 if os.path.isfile(os.path.join(cwd, filename)):
2580 return open(os.path.join(cwd, filename))
2581 if cwd == root:
2582 break
2583 cwd = os.path.dirname(cwd)
2584
2585
2586def LoadCodereviewSettingsFromFile(fileobj):
2587 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002588 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002589
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002590 def SetProperty(name, setting, unset_error_ok=False):
2591 fullname = 'rietveld.' + name
2592 if setting in keyvals:
2593 RunGit(['config', fullname, keyvals[setting]])
2594 else:
2595 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2596
2597 SetProperty('server', 'CODE_REVIEW_SERVER')
2598 # Only server setting is required. Other settings can be absent.
2599 # In that case, we ignore errors raised during option deletion attempt.
2600 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002601 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002602 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2603 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002604 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002605 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002606 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2607 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002608 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002609 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002610 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002611 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2612 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002613
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002614 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002615 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002616
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002617 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
2618 RunGit(['config', 'gerrit.squash-uploads',
2619 keyvals['GERRIT_SQUASH_UPLOADS']])
2620
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002621 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2622 #should be of the form
2623 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2624 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2625 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2626 keyvals['ORIGIN_URL_CONFIG']])
2627
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002628
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002629def urlretrieve(source, destination):
2630 """urllib is broken for SSL connections via a proxy therefore we
2631 can't use urllib.urlretrieve()."""
2632 with open(destination, 'w') as f:
2633 f.write(urllib2.urlopen(source).read())
2634
2635
ukai@chromium.org712d6102013-11-27 00:52:58 +00002636def hasSheBang(fname):
2637 """Checks fname is a #! script."""
2638 with open(fname) as f:
2639 return f.read(2).startswith('#!')
2640
2641
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00002642# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
2643def DownloadHooks(*args, **kwargs):
2644 pass
2645
2646
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002647def DownloadGerritHook(force):
2648 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002649
2650 Args:
2651 force: True to update hooks. False to install hooks if not present.
2652 """
2653 if not settings.GetIsGerrit():
2654 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00002655 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002656 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2657 if not os.access(dst, os.X_OK):
2658 if os.path.exists(dst):
2659 if not force:
2660 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002661 try:
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002662 print(
2663 'WARNING: installing Gerrit commit-msg hook.\n'
2664 ' This behavior of git cl will soon be disabled.\n'
2665 ' See bug http://crbug.com/579176.')
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002666 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002667 if not hasSheBang(dst):
2668 DieWithError('Not a script: %s\n'
2669 'You need to download from\n%s\n'
2670 'into .git/hooks/commit-msg and '
2671 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002672 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2673 except Exception:
2674 if os.path.exists(dst):
2675 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002676 DieWithError('\nFailed to download hooks.\n'
2677 'You need to download from\n%s\n'
2678 'into .git/hooks/commit-msg and '
2679 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002680
2681
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002682
2683def GetRietveldCodereviewSettingsInteractively():
2684 """Prompt the user for settings."""
2685 server = settings.GetDefaultServerUrl(error_ok=True)
2686 prompt = 'Rietveld server (host[:port])'
2687 prompt += ' [%s]' % (server or DEFAULT_SERVER)
2688 newserver = ask_for_data(prompt + ':')
2689 if not server and not newserver:
2690 newserver = DEFAULT_SERVER
2691 if newserver:
2692 newserver = gclient_utils.UpgradeToHttps(newserver)
2693 if newserver != server:
2694 RunGit(['config', 'rietveld.server', newserver])
2695
2696 def SetProperty(initial, caption, name, is_url):
2697 prompt = caption
2698 if initial:
2699 prompt += ' ("x" to clear) [%s]' % initial
2700 new_val = ask_for_data(prompt + ':')
2701 if new_val == 'x':
2702 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
2703 elif new_val:
2704 if is_url:
2705 new_val = gclient_utils.UpgradeToHttps(new_val)
2706 if new_val != initial:
2707 RunGit(['config', 'rietveld.' + name, new_val])
2708
2709 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
2710 SetProperty(settings.GetDefaultPrivateFlag(),
2711 'Private flag (rietveld only)', 'private', False)
2712 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
2713 'tree-status-url', False)
2714 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
2715 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
2716 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
2717 'run-post-upload-hook', False)
2718
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002719@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002720def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002721 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002722
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002723 print('WARNING: git cl config works for Rietveld only.\n'
2724 'For Gerrit, see http://crbug.com/579160.')
2725 # TODO(tandrii): add Gerrit support as part of http://crbug.com/579160.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00002726 parser.add_option('--activate-update', action='store_true',
2727 help='activate auto-updating [rietveld] section in '
2728 '.git/config')
2729 parser.add_option('--deactivate-update', action='store_true',
2730 help='deactivate auto-updating [rietveld] section in '
2731 '.git/config')
2732 options, args = parser.parse_args(args)
2733
2734 if options.deactivate_update:
2735 RunGit(['config', 'rietveld.autoupdate', 'false'])
2736 return
2737
2738 if options.activate_update:
2739 RunGit(['config', '--unset', 'rietveld.autoupdate'])
2740 return
2741
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002742 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002743 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002744 return 0
2745
2746 url = args[0]
2747 if not url.endswith('codereview.settings'):
2748 url = os.path.join(url, 'codereview.settings')
2749
2750 # Load code review settings and download hooks (if available).
2751 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
2752 return 0
2753
2754
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002755def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002756 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002757 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
2758 branch = ShortBranchName(branchref)
2759 _, args = parser.parse_args(args)
2760 if not args:
2761 print("Current base-url:")
2762 return RunGit(['config', 'branch.%s.base-url' % branch],
2763 error_ok=False).strip()
2764 else:
2765 print("Setting base-url to %s" % args[0])
2766 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
2767 error_ok=False).strip()
2768
2769
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002770def color_for_status(status):
2771 """Maps a Changelist status to color, for CMDstatus and other tools."""
2772 return {
2773 'unsent': Fore.RED,
2774 'waiting': Fore.BLUE,
2775 'reply': Fore.YELLOW,
2776 'lgtm': Fore.GREEN,
2777 'commit': Fore.MAGENTA,
2778 'closed': Fore.CYAN,
2779 'error': Fore.WHITE,
2780 }.get(status, Fore.WHITE)
2781
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002782def fetch_cl_status(branch, auth_config=None):
2783 """Fetches information for an issue and returns (branch, issue, status)."""
2784 cl = Changelist(branchref=branch, auth_config=auth_config)
2785 url = cl.GetIssueURL()
2786 status = cl.GetStatus()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002787
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002788 if url and (not status or status == 'error'):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002789 # The issue probably doesn't exist anymore.
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002790 url += ' (broken)'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002791
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002792 return (branch, url, status)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002793
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002794def get_cl_statuses(
2795 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002796 """Returns a blocking iterable of (branch, issue, color) for given branches.
2797
2798 If fine_grained is true, this will fetch CL statuses from the server.
2799 Otherwise, simply indicate if there's a matching url for the given branches.
2800
2801 If max_processes is specified, it is used as the maximum number of processes
2802 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
2803 spawned.
2804 """
2805 # Silence upload.py otherwise it becomes unwieldly.
2806 upload.verbosity = 0
2807
2808 if fine_grained:
2809 # Process one branch synchronously to work through authentication, then
2810 # spawn processes to process all the other branches in parallel.
2811 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002812 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
2813 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002814
2815 branches_to_fetch = branches[1:]
2816 pool = ThreadPool(
2817 min(max_processes, len(branches_to_fetch))
2818 if max_processes is not None
2819 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002820 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002821 yield x
2822 else:
2823 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
2824 for b in branches:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002825 cl = Changelist(branchref=b, auth_config=auth_config)
2826 url = cl.GetIssueURL()
2827 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002828
rmistry@google.com2dd99862015-06-22 12:22:18 +00002829
2830def upload_branch_deps(cl, args):
2831 """Uploads CLs of local branches that are dependents of the current branch.
2832
2833 If the local branch dependency tree looks like:
2834 test1 -> test2.1 -> test3.1
2835 -> test3.2
2836 -> test2.2 -> test3.3
2837
2838 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
2839 run on the dependent branches in this order:
2840 test2.1, test3.1, test3.2, test2.2, test3.3
2841
2842 Note: This function does not rebase your local dependent branches. Use it when
2843 you make a change to the parent branch that will not conflict with its
2844 dependent branches, and you would like their dependencies updated in
2845 Rietveld.
2846 """
2847 if git_common.is_dirty_git_tree('upload-branch-deps'):
2848 return 1
2849
2850 root_branch = cl.GetBranch()
2851 if root_branch is None:
2852 DieWithError('Can\'t find dependent branches from detached HEAD state. '
2853 'Get on a branch!')
2854 if not cl.GetIssue() or not cl.GetPatchset():
2855 DieWithError('Current branch does not have an uploaded CL. We cannot set '
2856 'patchset dependencies without an uploaded CL.')
2857
2858 branches = RunGit(['for-each-ref',
2859 '--format=%(refname:short) %(upstream:short)',
2860 'refs/heads'])
2861 if not branches:
2862 print('No local branches found.')
2863 return 0
2864
2865 # Create a dictionary of all local branches to the branches that are dependent
2866 # on it.
2867 tracked_to_dependents = collections.defaultdict(list)
2868 for b in branches.splitlines():
2869 tokens = b.split()
2870 if len(tokens) == 2:
2871 branch_name, tracked = tokens
2872 tracked_to_dependents[tracked].append(branch_name)
2873
2874 print
2875 print 'The dependent local branches of %s are:' % root_branch
2876 dependents = []
2877 def traverse_dependents_preorder(branch, padding=''):
2878 dependents_to_process = tracked_to_dependents.get(branch, [])
2879 padding += ' '
2880 for dependent in dependents_to_process:
2881 print '%s%s' % (padding, dependent)
2882 dependents.append(dependent)
2883 traverse_dependents_preorder(dependent, padding)
2884 traverse_dependents_preorder(root_branch)
2885 print
2886
2887 if not dependents:
2888 print 'There are no dependent local branches for %s' % root_branch
2889 return 0
2890
2891 print ('This command will checkout all dependent branches and run '
2892 '"git cl upload".')
2893 ask_for_data('[Press enter to continue or ctrl-C to quit]')
2894
andybons@chromium.org962f9462016-02-03 20:00:42 +00002895 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00002896 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00002897 args.extend(['-t', 'Updated patchset dependency'])
2898
rmistry@google.com2dd99862015-06-22 12:22:18 +00002899 # Record all dependents that failed to upload.
2900 failures = {}
2901 # Go through all dependents, checkout the branch and upload.
2902 try:
2903 for dependent_branch in dependents:
2904 print
2905 print '--------------------------------------'
2906 print 'Running "git cl upload" from %s:' % dependent_branch
2907 RunGit(['checkout', '-q', dependent_branch])
2908 print
2909 try:
2910 if CMDupload(OptionParser(), args) != 0:
2911 print 'Upload failed for %s!' % dependent_branch
2912 failures[dependent_branch] = 1
2913 except: # pylint: disable=W0702
2914 failures[dependent_branch] = 1
2915 print
2916 finally:
2917 # Swap back to the original root branch.
2918 RunGit(['checkout', '-q', root_branch])
2919
2920 print
2921 print 'Upload complete for dependent branches!'
2922 for dependent_branch in dependents:
2923 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
2924 print ' %s : %s' % (dependent_branch, upload_status)
2925 print
2926
2927 return 0
2928
2929
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002930def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002931 """Show status of changelists.
2932
2933 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00002934 - Red not sent for review or broken
2935 - Blue waiting for review
2936 - Yellow waiting for you to reply to review
2937 - Green LGTM'ed
2938 - Magenta in the commit queue
2939 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002940
2941 Also see 'git cl comments'.
2942 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002943 parser.add_option('--field',
2944 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002945 parser.add_option('-f', '--fast', action='store_true',
2946 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002947 parser.add_option(
2948 '-j', '--maxjobs', action='store', type=int,
2949 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002950
2951 auth.add_auth_options(parser)
2952 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002953 if args:
2954 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002955 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002956
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002957 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002958 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002959 if options.field.startswith('desc'):
2960 print cl.GetDescription()
2961 elif options.field == 'id':
2962 issueid = cl.GetIssue()
2963 if issueid:
2964 print issueid
2965 elif options.field == 'patch':
2966 patchset = cl.GetPatchset()
2967 if patchset:
2968 print patchset
2969 elif options.field == 'url':
2970 url = cl.GetIssueURL()
2971 if url:
2972 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002973 return 0
2974
2975 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
2976 if not branches:
2977 print('No local branch found.')
2978 return 0
2979
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002980 changes = (
2981 Changelist(branchref=b, auth_config=auth_config)
2982 for b in branches.splitlines())
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002983 # TODO(tandrii): refactor to use CLs list instead of branches list.
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00002984 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002985 alignment = max(5, max(len(b) for b in branches))
2986 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002987 output = get_cl_statuses(branches,
2988 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002989 max_processes=options.maxjobs,
2990 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002991
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002992 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002993 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002994 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002995 while branch not in branch_statuses:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002996 b, i, status = output.next()
2997 branch_statuses[b] = (i, status)
2998 issue_url, status = branch_statuses.pop(branch)
2999 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003000 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003001 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003002 color = ''
3003 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003004 status_str = '(%s)' % status if status else ''
3005 print ' %*s : %s%s %s%s' % (
3006 alignment, ShortBranchName(branch), color, issue_url, status_str,
3007 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003008
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003009 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003010 print
3011 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003012 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003013 if not cl.GetIssue():
3014 print 'No issue assigned.'
3015 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003016 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00003017 if not options.fast:
3018 print 'Issue description:'
3019 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003020 return 0
3021
3022
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003023def colorize_CMDstatus_doc():
3024 """To be called once in main() to add colors to git cl status help."""
3025 colors = [i for i in dir(Fore) if i[0].isupper()]
3026
3027 def colorize_line(line):
3028 for color in colors:
3029 if color in line.upper():
3030 # Extract whitespaces first and the leading '-'.
3031 indent = len(line) - len(line.lstrip(' ')) + 1
3032 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3033 return line
3034
3035 lines = CMDstatus.__doc__.splitlines()
3036 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3037
3038
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003039@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003040def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003041 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003042
3043 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003044 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003045 parser.add_option('-r', '--reverse', action='store_true',
3046 help='Lookup the branch(es) for the specified issues. If '
3047 'no issues are specified, all branches with mapped '
3048 'issues will be listed.')
3049 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003050
dnj@chromium.org406c4402015-03-03 17:22:28 +00003051 if options.reverse:
3052 branches = RunGit(['for-each-ref', 'refs/heads',
3053 '--format=%(refname:short)']).splitlines()
3054
3055 # Reverse issue lookup.
3056 issue_branch_map = {}
3057 for branch in branches:
3058 cl = Changelist(branchref=branch)
3059 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3060 if not args:
3061 args = sorted(issue_branch_map.iterkeys())
3062 for issue in args:
3063 if not issue:
3064 continue
3065 print 'Branch for issue number %s: %s' % (
3066 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
3067 else:
3068 cl = Changelist()
3069 if len(args) > 0:
3070 try:
3071 issue = int(args[0])
3072 except ValueError:
3073 DieWithError('Pass a number to set the issue or none to list it.\n'
3074 'Maybe you want to run git cl status?')
3075 cl.SetIssue(issue)
3076 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003077 return 0
3078
3079
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003080def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003081 """Shows or posts review comments for any changelist."""
3082 parser.add_option('-a', '--add-comment', dest='comment',
3083 help='comment to add to an issue')
3084 parser.add_option('-i', dest='issue',
3085 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003086 parser.add_option('-j', '--json-file',
3087 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003088 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003089 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003090 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003091
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003092 issue = None
3093 if options.issue:
3094 try:
3095 issue = int(options.issue)
3096 except ValueError:
3097 DieWithError('A review issue id is expected to be a number')
3098
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003099 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003100
3101 if options.comment:
3102 cl.AddComment(options.comment)
3103 return 0
3104
3105 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003106 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003107 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003108 summary.append({
3109 'date': message['date'],
3110 'lgtm': False,
3111 'message': message['text'],
3112 'not_lgtm': False,
3113 'sender': message['sender'],
3114 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003115 if message['disapproval']:
3116 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003117 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003118 elif message['approval']:
3119 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003120 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003121 elif message['sender'] == data['owner_email']:
3122 color = Fore.MAGENTA
3123 else:
3124 color = Fore.BLUE
3125 print '\n%s%s %s%s' % (
3126 color, message['date'].split('.', 1)[0], message['sender'],
3127 Fore.RESET)
3128 if message['text'].strip():
3129 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00003130 if options.json_file:
3131 with open(options.json_file, 'wb') as f:
3132 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003133 return 0
3134
3135
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003136def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003137 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003138 parser.add_option('-d', '--display', action='store_true',
3139 help='Display the description instead of opening an editor')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003140 auth.add_auth_options(parser)
3141 options, _ = parser.parse_args(args)
3142 auth_config = auth.extract_auth_config_from_options(options)
3143 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003144 if not cl.GetIssue():
3145 DieWithError('This branch has no associated changelist.')
3146 description = ChangeDescription(cl.GetDescription())
smut@google.com34fb6b12015-07-13 20:03:26 +00003147 if options.display:
3148 print description.description
3149 return 0
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003150 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003151 if cl.GetDescription() != description.description:
3152 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003153 return 0
3154
3155
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003156def CreateDescriptionFromLog(args):
3157 """Pulls out the commit log to use as a base for the CL description."""
3158 log_args = []
3159 if len(args) == 1 and not args[0].endswith('.'):
3160 log_args = [args[0] + '..']
3161 elif len(args) == 1 and args[0].endswith('...'):
3162 log_args = [args[0][:-1]]
3163 elif len(args) == 2:
3164 log_args = [args[0] + '..' + args[1]]
3165 else:
3166 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003167 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003168
3169
thestig@chromium.org44202a22014-03-11 19:22:18 +00003170def CMDlint(parser, args):
3171 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003172 parser.add_option('--filter', action='append', metavar='-x,+y',
3173 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003174 auth.add_auth_options(parser)
3175 options, args = parser.parse_args(args)
3176 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003177
3178 # Access to a protected member _XX of a client class
3179 # pylint: disable=W0212
3180 try:
3181 import cpplint
3182 import cpplint_chromium
3183 except ImportError:
3184 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
3185 return 1
3186
3187 # Change the current working directory before calling lint so that it
3188 # shows the correct base.
3189 previous_cwd = os.getcwd()
3190 os.chdir(settings.GetRoot())
3191 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003192 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003193 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3194 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003195 if not files:
3196 print "Cannot lint an empty CL"
3197 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003198
3199 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003200 command = args + files
3201 if options.filter:
3202 command = ['--filter=' + ','.join(options.filter)] + command
3203 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003204
3205 white_regex = re.compile(settings.GetLintRegex())
3206 black_regex = re.compile(settings.GetLintIgnoreRegex())
3207 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3208 for filename in filenames:
3209 if white_regex.match(filename):
3210 if black_regex.match(filename):
3211 print "Ignoring file %s" % filename
3212 else:
3213 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3214 extra_check_functions)
3215 else:
3216 print "Skipping file %s" % filename
3217 finally:
3218 os.chdir(previous_cwd)
3219 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
3220 if cpplint._cpplint_state.error_count != 0:
3221 return 1
3222 return 0
3223
3224
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003225def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003226 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003227 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003228 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003229 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003230 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003231 auth.add_auth_options(parser)
3232 options, args = parser.parse_args(args)
3233 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003234
sbc@chromium.org71437c02015-04-09 19:29:40 +00003235 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00003236 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003237 return 1
3238
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003239 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003240 if args:
3241 base_branch = args[0]
3242 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003243 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003244 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003245
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003246 cl.RunHook(
3247 committing=not options.upload,
3248 may_prompt=False,
3249 verbose=options.verbose,
3250 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003251 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003252
3253
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00003254def AddChangeIdToCommitMessage(options, args):
3255 """Re-commits using the current message, assumes the commit hook is in
3256 place.
3257 """
3258 log_desc = options.message or CreateDescriptionFromLog(args)
3259 git_command = ['commit', '--amend', '-m', log_desc]
3260 RunGit(git_command)
3261 new_log_desc = CreateDescriptionFromLog(args)
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003262 if git_footers.get_footer_change_id(new_log_desc):
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00003263 print 'git-cl: Added Change-Id to commit message.'
tandrii@chromium.orga342c922016-03-16 07:08:25 +00003264 return new_log_desc
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00003265 else:
3266 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
3267
3268
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003269def GenerateGerritChangeId(message):
3270 """Returns Ixxxxxx...xxx change id.
3271
3272 Works the same way as
3273 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3274 but can be called on demand on all platforms.
3275
3276 The basic idea is to generate git hash of a state of the tree, original commit
3277 message, author/committer info and timestamps.
3278 """
3279 lines = []
3280 tree_hash = RunGitSilent(['write-tree'])
3281 lines.append('tree %s' % tree_hash.strip())
3282 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3283 if code == 0:
3284 lines.append('parent %s' % parent.strip())
3285 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3286 lines.append('author %s' % author.strip())
3287 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3288 lines.append('committer %s' % committer.strip())
3289 lines.append('')
3290 # Note: Gerrit's commit-hook actually cleans message of some lines and
3291 # whitespace. This code is not doing this, but it clearly won't decrease
3292 # entropy.
3293 lines.append(message)
3294 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3295 stdin='\n'.join(lines))
3296 return 'I%s' % change_hash.strip()
3297
3298
wittman@chromium.org455dc922015-01-26 20:15:50 +00003299def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3300 """Computes the remote branch ref to use for the CL.
3301
3302 Args:
3303 remote (str): The git remote for the CL.
3304 remote_branch (str): The git remote branch for the CL.
3305 target_branch (str): The target branch specified by the user.
3306 pending_prefix (str): The pending prefix from the settings.
3307 """
3308 if not (remote and remote_branch):
3309 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003310
wittman@chromium.org455dc922015-01-26 20:15:50 +00003311 if target_branch:
3312 # Cannonicalize branch references to the equivalent local full symbolic
3313 # refs, which are then translated into the remote full symbolic refs
3314 # below.
3315 if '/' not in target_branch:
3316 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3317 else:
3318 prefix_replacements = (
3319 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3320 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3321 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3322 )
3323 match = None
3324 for regex, replacement in prefix_replacements:
3325 match = re.search(regex, target_branch)
3326 if match:
3327 remote_branch = target_branch.replace(match.group(0), replacement)
3328 break
3329 if not match:
3330 # This is a branch path but not one we recognize; use as-is.
3331 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003332 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3333 # Handle the refs that need to land in different refs.
3334 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003335
wittman@chromium.org455dc922015-01-26 20:15:50 +00003336 # Create the true path to the remote branch.
3337 # Does the following translation:
3338 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3339 # * refs/remotes/origin/master -> refs/heads/master
3340 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3341 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3342 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3343 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3344 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3345 'refs/heads/')
3346 elif remote_branch.startswith('refs/remotes/branch-heads'):
3347 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3348 # If a pending prefix exists then replace refs/ with it.
3349 if pending_prefix:
3350 remote_branch = remote_branch.replace('refs/', pending_prefix)
3351 return remote_branch
3352
3353
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003354def cleanup_list(l):
3355 """Fixes a list so that comma separated items are put as individual items.
3356
3357 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3358 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3359 """
3360 items = sum((i.split(',') for i in l), [])
3361 stripped_items = (i.strip() for i in items)
3362 return sorted(filter(None, stripped_items))
3363
3364
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003365@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003366def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003367 """Uploads the current changelist to codereview.
3368
3369 Can skip dependency patchset uploads for a branch by running:
3370 git config branch.branch_name.skip-deps-uploads True
3371 To unset run:
3372 git config --unset branch.branch_name.skip-deps-uploads
3373 Can also set the above globally by using the --global flag.
3374 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003375 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3376 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003377 parser.add_option('--bypass-watchlists', action='store_true',
3378 dest='bypass_watchlists',
3379 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003380 parser.add_option('-f', action='store_true', dest='force',
3381 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003382 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003383 parser.add_option('-t', dest='title',
3384 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003385 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003386 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003387 help='reviewer email addresses')
3388 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003389 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003390 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003391 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003392 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003393 parser.add_option('--emulate_svn_auto_props',
3394 '--emulate-svn-auto-props',
3395 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003396 dest="emulate_svn_auto_props",
3397 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003398 parser.add_option('-c', '--use-commit-queue', action='store_true',
3399 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003400 parser.add_option('--private', action='store_true',
3401 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003402 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003403 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003404 metavar='TARGET',
3405 help='Apply CL to remote ref TARGET. ' +
3406 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003407 parser.add_option('--squash', action='store_true',
3408 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003409 parser.add_option('--no-squash', action='store_true',
3410 help='Don\'t squash multiple commits into one ' +
3411 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003412 parser.add_option('--email', default=None,
3413 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003414 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3415 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003416 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3417 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003418 help='Send the patchset to do a CQ dry run right after '
3419 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003420 parser.add_option('--dependencies', action='store_true',
3421 help='Uploads CLs of all the local branches that depend on '
3422 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003423
rmistry@google.com2dd99862015-06-22 12:22:18 +00003424 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003425 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003426 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003427 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003428 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003429
sbc@chromium.org71437c02015-04-09 19:29:40 +00003430 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003431 return 1
3432
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003433 options.reviewers = cleanup_list(options.reviewers)
3434 options.cc = cleanup_list(options.cc)
3435
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003436 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3437 settings.GetIsGerrit()
3438
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003439 cl = Changelist(auth_config=auth_config)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00003440 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003441
3442
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003443def IsSubmoduleMergeCommit(ref):
3444 # When submodules are added to the repo, we expect there to be a single
3445 # non-git-svn merge commit at remote HEAD with a signature comment.
3446 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003447 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003448 return RunGit(cmd) != ''
3449
3450
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003451def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003452 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003453
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003454 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3455 upstream and closes the issue automatically and atomically.
3456
3457 Otherwise (in case of Rietveld):
3458 Squashes branch into a single commit.
3459 Updates changelog with metadata (e.g. pointer to review).
3460 Pushes/dcommits the code upstream.
3461 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003462 """
3463 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3464 help='bypass upload presubmit hook')
3465 parser.add_option('-m', dest='message',
3466 help="override review description")
3467 parser.add_option('-f', action='store_true', dest='force',
3468 help="force yes to questions (don't prompt)")
3469 parser.add_option('-c', dest='contributor',
3470 help="external contributor for patch (appended to " +
3471 "description and used as author for git). Should be " +
3472 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003473 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003474 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003475 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003476 auth_config = auth.extract_auth_config_from_options(options)
3477
3478 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003479
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003480 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3481 if cl.IsGerrit():
3482 if options.message:
3483 # This could be implemented, but it requires sending a new patch to
3484 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3485 # Besides, Gerrit has the ability to change the commit message on submit
3486 # automatically, thus there is no need to support this option (so far?).
3487 parser.error('-m MESSAGE option is not supported for Gerrit.')
3488 if options.contributor:
3489 parser.error(
3490 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3491 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3492 'the contributor\'s "name <email>". If you can\'t upload such a '
3493 'commit for review, contact your repository admin and request'
3494 '"Forge-Author" permission.')
3495 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3496 options.verbose)
3497
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003498 current = cl.GetBranch()
3499 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3500 if not settings.GetIsGitSvn() and remote == '.':
3501 print
3502 print 'Attempting to push branch %r into another local branch!' % current
3503 print
3504 print 'Either reparent this branch on top of origin/master:'
3505 print ' git reparent-branch --root'
3506 print
3507 print 'OR run `git rebase-update` if you think the parent branch is already'
3508 print 'committed.'
3509 print
3510 print ' Current parent: %r' % upstream_branch
3511 return 1
3512
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003513 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003514 # Default to merging against our best guess of the upstream branch.
3515 args = [cl.GetUpstreamBranch()]
3516
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003517 if options.contributor:
3518 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
3519 print "Please provide contibutor as 'First Last <email@example.com>'"
3520 return 1
3521
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003522 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003523 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003524
sbc@chromium.org71437c02015-04-09 19:29:40 +00003525 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003526 return 1
3527
3528 # This rev-list syntax means "show all commits not in my branch that
3529 # are in base_branch".
3530 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3531 base_branch]).splitlines()
3532 if upstream_commits:
3533 print ('Base branch "%s" has %d commits '
3534 'not in this branch.' % (base_branch, len(upstream_commits)))
3535 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
3536 return 1
3537
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003538 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003539 svn_head = None
3540 if cmd == 'dcommit' or base_has_submodules:
3541 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
3542 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003543
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003544 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003545 # If the base_head is a submodule merge commit, the first parent of the
3546 # base_head should be a git-svn commit, which is what we're interested in.
3547 base_svn_head = base_branch
3548 if base_has_submodules:
3549 base_svn_head += '^1'
3550
3551 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003552 if extra_commits:
3553 print ('This branch has %d additional commits not upstreamed yet.'
3554 % len(extra_commits.splitlines()))
3555 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
3556 'before attempting to %s.' % (base_branch, cmd))
3557 return 1
3558
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003559 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003560 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003561 author = None
3562 if options.contributor:
3563 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003564 hook_results = cl.RunHook(
3565 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003566 may_prompt=not options.force,
3567 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003568 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003569 if not hook_results.should_continue():
3570 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003571
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003572 # Check the tree status if the tree status URL is set.
3573 status = GetTreeStatus()
3574 if 'closed' == status:
3575 print('The tree is closed. Please wait for it to reopen. Use '
3576 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3577 return 1
3578 elif 'unknown' == status:
3579 print('Unable to determine tree status. Please verify manually and '
3580 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3581 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003582
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003583 change_desc = ChangeDescription(options.message)
3584 if not change_desc.description and cl.GetIssue():
3585 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003586
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003587 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00003588 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003589 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00003590 else:
3591 print 'No description set.'
3592 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
3593 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003594
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003595 # Keep a separate copy for the commit message, because the commit message
3596 # contains the link to the Rietveld issue, while the Rietveld message contains
3597 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003598 # Keep a separate copy for the commit message.
3599 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00003600 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003601
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003602 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00003603 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00003604 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00003605 # after it. Add a period on a new line to circumvent this. Also add a space
3606 # before the period to make sure that Gitiles continues to correctly resolve
3607 # the URL.
3608 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003609 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003610 commit_desc.append_footer('Patch from %s.' % options.contributor)
3611
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00003612 print('Description:')
3613 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003614
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003615 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003616 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00003617 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003618
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003619 # We want to squash all this branch's commits into one commit with the proper
3620 # description. We do this by doing a "reset --soft" to the base branch (which
3621 # keeps the working copy the same), then dcommitting that. If origin/master
3622 # has a submodule merge commit, we'll also need to cherry-pick the squashed
3623 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003624 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003625 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
3626 # Delete the branches if they exist.
3627 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
3628 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
3629 result = RunGitWithCode(showref_cmd)
3630 if result[0] == 0:
3631 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003632
3633 # We might be in a directory that's present in this branch but not in the
3634 # trunk. Move up to the top of the tree so that git commands that expect a
3635 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003636 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003637 if rel_base_path:
3638 os.chdir(rel_base_path)
3639
3640 # Stuff our change into the merge branch.
3641 # We wrap in a try...finally block so if anything goes wrong,
3642 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003643 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003644 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003645 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003646 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003647 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00003648 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003649 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003650 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003651 RunGit(
3652 [
3653 'commit', '--author', options.contributor,
3654 '-m', commit_desc.description,
3655 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003656 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003657 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003658 if base_has_submodules:
3659 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
3660 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
3661 RunGit(['checkout', CHERRY_PICK_BRANCH])
3662 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003663 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003664 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003665 mirror = settings.GetGitMirror(remote)
3666 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003667 pending_prefix = settings.GetPendingRefPrefix()
3668 if not pending_prefix or branch.startswith(pending_prefix):
3669 # If not using refs/pending/heads/* at all, or target ref is already set
3670 # to pending, then push to the target ref directly.
3671 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003672 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003673 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003674 else:
3675 # Cherry-pick the change on top of pending ref and then push it.
3676 assert branch.startswith('refs/'), branch
3677 assert pending_prefix[-1] == '/', pending_prefix
3678 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003679 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003680 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003681 if retcode == 0:
3682 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003683 else:
3684 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003685 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003686 'svn', 'dcommit',
3687 '-C%s' % options.similarity,
3688 '--no-rebase', '--rmdir',
3689 ]
3690 if settings.GetForceHttpsCommitUrl():
3691 # Allow forcing https commit URLs for some projects that don't allow
3692 # committing to http URLs (like Google Code).
3693 remote_url = cl.GetGitSvnRemoteUrl()
3694 if urlparse.urlparse(remote_url).scheme == 'http':
3695 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003696 cmd_args.append('--commit-url=%s' % remote_url)
3697 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003698 if 'Committed r' in output:
3699 revision = re.match(
3700 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
3701 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003702 finally:
3703 # And then swap back to the original branch and clean up.
3704 RunGit(['checkout', '-q', cl.GetBranch()])
3705 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003706 if base_has_submodules:
3707 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003708
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003709 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003710 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003711 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003712
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003713 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003714 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003715 try:
3716 revision = WaitForRealCommit(remote, revision, base_branch, branch)
3717 # We set pushed_to_pending to False, since it made it all the way to the
3718 # real ref.
3719 pushed_to_pending = False
3720 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003721 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003722
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003723 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003724 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003725 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003726 if not to_pending:
3727 if viewvc_url and revision:
3728 change_desc.append_footer(
3729 'Committed: %s%s' % (viewvc_url, revision))
3730 elif revision:
3731 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003732 print ('Closing issue '
3733 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003734 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003735 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003736 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00003737 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00003738 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00003739 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003740 if options.bypass_hooks:
3741 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
3742 else:
3743 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00003744 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003745 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003746
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003747 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003748 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
3749 print 'The commit is in the pending queue (%s).' % pending_ref
3750 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00003751 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003752 'footer.' % branch)
3753
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003754 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
3755 if os.path.isfile(hook):
3756 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003757
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003758 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003759
3760
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003761def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
3762 print
3763 print 'Waiting for commit to be landed on %s...' % real_ref
3764 print '(If you are impatient, you may Ctrl-C once without harm)'
3765 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
3766 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003767 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003768
3769 loop = 0
3770 while True:
3771 sys.stdout.write('fetching (%d)... \r' % loop)
3772 sys.stdout.flush()
3773 loop += 1
3774
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003775 if mirror:
3776 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003777 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
3778 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
3779 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
3780 for commit in commits.splitlines():
3781 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
3782 print 'Found commit on %s' % real_ref
3783 return commit
3784
3785 current_rev = to_rev
3786
3787
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003788def PushToGitPending(remote, pending_ref, upstream_ref):
3789 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
3790
3791 Returns:
3792 (retcode of last operation, output log of last operation).
3793 """
3794 assert pending_ref.startswith('refs/'), pending_ref
3795 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
3796 cherry = RunGit(['rev-parse', 'HEAD']).strip()
3797 code = 0
3798 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003799 max_attempts = 3
3800 attempts_left = max_attempts
3801 while attempts_left:
3802 if attempts_left != max_attempts:
3803 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
3804 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003805
3806 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003807 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003808 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003809 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003810 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003811 print 'Fetch failed with exit code %d.' % code
3812 if out.strip():
3813 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003814 continue
3815
3816 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003817 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003818 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003819 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003820 if code:
3821 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003822 'Your patch doesn\'t apply cleanly to ref \'%s\', '
3823 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003824 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
3825 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003826 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003827 return code, out
3828
3829 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003830 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003831 code, out = RunGitWithCode(
3832 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
3833 if code == 0:
3834 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003835 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003836 return code, out
3837
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003838 print 'Push failed with exit code %d.' % code
3839 if out.strip():
3840 print out.strip()
3841 if IsFatalPushFailure(out):
3842 print (
3843 'Fatal push error. Make sure your .netrc credentials and git '
3844 'user.email are correct and you have push access to the repo.')
3845 return code, out
3846
3847 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003848 return code, out
3849
3850
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003851def IsFatalPushFailure(push_stdout):
3852 """True if retrying push won't help."""
3853 return '(prohibited by Gerrit)' in push_stdout
3854
3855
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003856@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003857def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003858 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003859 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003860 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003861 # If it looks like previous commits were mirrored with git-svn.
3862 message = """This repository appears to be a git-svn mirror, but no
3863upstream SVN master is set. You probably need to run 'git auto-svn' once."""
3864 else:
3865 message = """This doesn't appear to be an SVN repository.
3866If your project has a true, writeable git repository, you probably want to run
3867'git cl land' instead.
3868If your project has a git mirror of an upstream SVN master, you probably need
3869to run 'git svn init'.
3870
3871Using the wrong command might cause your commit to appear to succeed, and the
3872review to be closed, without actually landing upstream. If you choose to
3873proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00003874 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00003875 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003876 return SendUpstream(parser, args, 'dcommit')
3877
3878
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003879@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003880def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003881 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003882 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003883 print('This appears to be an SVN repository.')
3884 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003885 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00003886 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003887 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003888
3889
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003890@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003891def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00003892 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003893 parser.add_option('-b', dest='newbranch',
3894 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003895 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003896 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003897 parser.add_option('-d', '--directory', action='store', metavar='DIR',
3898 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003899 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003900 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003901 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003902 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003903 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003904 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003905
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003906
3907 group = optparse.OptionGroup(
3908 parser,
3909 'Options for continuing work on the current issue uploaded from a '
3910 'different clone (e.g. different machine). Must be used independently '
3911 'from the other options. No issue number should be specified, and the '
3912 'branch must have an issue number associated with it')
3913 group.add_option('--reapply', action='store_true', dest='reapply',
3914 help='Reset the branch and reapply the issue.\n'
3915 'CAUTION: This will undo any local changes in this '
3916 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003917
3918 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003919 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003920 parser.add_option_group(group)
3921
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003922 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003923 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003924 auth_config = auth.extract_auth_config_from_options(options)
3925
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003926 cl = Changelist(auth_config=auth_config)
3927
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003928 issue_arg = None
3929 if options.reapply :
3930 if len(args) > 0:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003931 parser.error('--reapply implies no additional arguments.')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003932
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003933 issue_arg = cl.GetIssue()
3934 upstream = cl.GetUpstreamBranch()
3935 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003936 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003937
3938 RunGit(['reset', '--hard', upstream])
3939 if options.pull:
3940 RunGit(['pull'])
3941 else:
3942 if len(args) != 1:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003943 parser.error('Must specify issue number or url')
3944 issue_arg = args[0]
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003945
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003946 if not issue_arg:
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003947 parser.print_help()
3948 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003949
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003950 if cl.IsGerrit():
3951 if options.reject:
3952 parser.error('--reject is not supported with Gerrit codereview.')
3953 if options.nocommit:
3954 parser.error('--nocommit is not supported with Gerrit codereview.')
3955 if options.directory:
3956 parser.error('--directory is not supported with Gerrit codereview.')
3957
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003958 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003959 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003960 return 1
3961
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003962 if options.newbranch:
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003963 if options.reapply:
3964 parser.error("--reapply excludes any option other than --pull")
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003965 if options.force:
3966 RunGit(['branch', '-D', options.newbranch],
3967 stderr=subprocess2.PIPE, error_ok=True)
3968 RunGit(['checkout', '-b', options.newbranch,
3969 Changelist().GetUpstreamBranch()])
3970
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003971 return cl.CMDPatchIssue(issue_arg, options.reject, options.nocommit,
3972 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003973
3974
3975def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003976 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003977 # Provide a wrapper for git svn rebase to help avoid accidental
3978 # git svn dcommit.
3979 # It's the only command that doesn't use parser at all since we just defer
3980 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003981
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003982 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003983
3984
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003985def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003986 """Fetches the tree status and returns either 'open', 'closed',
3987 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003988 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003989 if url:
3990 status = urllib2.urlopen(url).read().lower()
3991 if status.find('closed') != -1 or status == '0':
3992 return 'closed'
3993 elif status.find('open') != -1 or status == '1':
3994 return 'open'
3995 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003996 return 'unset'
3997
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003998
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003999def GetTreeStatusReason():
4000 """Fetches the tree status from a json url and returns the message
4001 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004002 url = settings.GetTreeStatusUrl()
4003 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004004 connection = urllib2.urlopen(json_url)
4005 status = json.loads(connection.read())
4006 connection.close()
4007 return status['message']
4008
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004009
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004010def GetBuilderMaster(bot_list):
4011 """For a given builder, fetch the master from AE if available."""
4012 map_url = 'https://builders-map.appspot.com/'
4013 try:
4014 master_map = json.load(urllib2.urlopen(map_url))
4015 except urllib2.URLError as e:
4016 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4017 (map_url, e))
4018 except ValueError as e:
4019 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4020 if not master_map:
4021 return None, 'Failed to build master map.'
4022
4023 result_master = ''
4024 for bot in bot_list:
4025 builder = bot.split(':', 1)[0]
4026 master_list = master_map.get(builder, [])
4027 if not master_list:
4028 return None, ('No matching master for builder %s.' % builder)
4029 elif len(master_list) > 1:
4030 return None, ('The builder name %s exists in multiple masters %s.' %
4031 (builder, master_list))
4032 else:
4033 cur_master = master_list[0]
4034 if not result_master:
4035 result_master = cur_master
4036 elif result_master != cur_master:
4037 return None, 'The builders do not belong to the same master.'
4038 return result_master, None
4039
4040
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004041def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004042 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004043 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004044 status = GetTreeStatus()
4045 if 'unset' == status:
4046 print 'You must configure your tree status URL by running "git cl config".'
4047 return 2
4048
4049 print "The tree is %s" % status
4050 print
4051 print GetTreeStatusReason()
4052 if status != 'open':
4053 return 1
4054 return 0
4055
4056
maruel@chromium.org15192402012-09-06 12:38:29 +00004057def CMDtry(parser, args):
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004058 """Triggers a try job through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00004059 group = optparse.OptionGroup(parser, "Try job options")
4060 group.add_option(
4061 "-b", "--bot", action="append",
4062 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
4063 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004064 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00004065 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004066 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00004067 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004068 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00004069 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004070 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004071 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004072 "-r", "--revision",
4073 help="Revision to use for the try job; default: the "
4074 "revision will be determined by the try server; see "
4075 "its waterfall for more info")
4076 group.add_option(
4077 "-c", "--clobber", action="store_true", default=False,
4078 help="Force a clobber before building; e.g. don't do an "
4079 "incremental build")
4080 group.add_option(
4081 "--project",
4082 help="Override which project to use. Projects are defined "
4083 "server-side to define what default bot set to use")
4084 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004085 "-p", "--property", dest="properties", action="append", default=[],
4086 help="Specify generic properties in the form -p key1=value1 -p "
4087 "key2=value2 etc (buildbucket only). The value will be treated as "
4088 "json if decodable, or as string otherwise.")
4089 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004090 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004091 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004092 "--use-rietveld", action="store_true", default=False,
4093 help="Use Rietveld to trigger try jobs.")
4094 group.add_option(
4095 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4096 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004097 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004098 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004099 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004100 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004101
machenbach@chromium.org45453142015-09-15 08:45:22 +00004102 if options.use_rietveld and options.properties:
4103 parser.error('Properties can only be specified with buildbucket')
4104
4105 # Make sure that all properties are prop=value pairs.
4106 bad_params = [x for x in options.properties if '=' not in x]
4107 if bad_params:
4108 parser.error('Got properties with missing "=": %s' % bad_params)
4109
maruel@chromium.org15192402012-09-06 12:38:29 +00004110 if args:
4111 parser.error('Unknown arguments: %s' % args)
4112
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004113 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004114 if not cl.GetIssue():
4115 parser.error('Need to upload first')
4116
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004117 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004118 if props.get('closed'):
4119 parser.error('Cannot send tryjobs for a closed CL')
4120
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004121 if props.get('private'):
4122 parser.error('Cannot use trybots with private issue')
4123
maruel@chromium.org15192402012-09-06 12:38:29 +00004124 if not options.name:
4125 options.name = cl.GetBranch()
4126
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004127 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004128 options.master, err_msg = GetBuilderMaster(options.bot)
4129 if err_msg:
4130 parser.error('Tryserver master cannot be found because: %s\n'
4131 'Please manually specify the tryserver master'
4132 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004133
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004134 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004135 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004136 if not options.bot:
4137 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004138
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004139 # Get try masters from PRESUBMIT.py files.
4140 masters = presubmit_support.DoGetTryMasters(
4141 change,
4142 change.LocalPaths(),
4143 settings.GetRoot(),
4144 None,
4145 None,
4146 options.verbose,
4147 sys.stdout)
4148 if masters:
4149 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004150
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004151 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4152 options.bot = presubmit_support.DoGetTrySlaves(
4153 change,
4154 change.LocalPaths(),
4155 settings.GetRoot(),
4156 None,
4157 None,
4158 options.verbose,
4159 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004160
4161 if not options.bot:
4162 # Get try masters from cq.cfg if any.
4163 # TODO(tandrii): some (but very few) projects store cq.cfg in different
4164 # location.
4165 cq_cfg = os.path.join(change.RepositoryRoot(),
4166 'infra', 'config', 'cq.cfg')
4167 if os.path.exists(cq_cfg):
4168 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00004169 cq_masters = commit_queue.get_master_builder_map(
4170 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004171 for master, builders in cq_masters.iteritems():
4172 for builder in builders:
4173 # Skip presubmit builders, because these will fail without LGTM.
4174 if 'presubmit' not in builder.lower():
4175 masters.setdefault(master, {})[builder] = ['defaulttests']
4176 if masters:
4177 return masters
4178
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004179 if not options.bot:
4180 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00004181
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004182 builders_and_tests = {}
4183 # TODO(machenbach): The old style command-line options don't support
4184 # multiple try masters yet.
4185 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4186 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4187
4188 for bot in old_style:
4189 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004190 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004191 elif ',' in bot:
4192 parser.error('Specify one bot per --bot flag')
4193 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004194 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004195
4196 for bot, tests in new_style:
4197 builders_and_tests.setdefault(bot, []).extend(tests)
4198
4199 # Return a master map with one master to be backwards compatible. The
4200 # master name defaults to an empty string, which will cause the master
4201 # not to be set on rietveld (deprecated).
4202 return {options.master: builders_and_tests}
4203
4204 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004205
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004206 for builders in masters.itervalues():
4207 if any('triggered' in b for b in builders):
4208 print >> sys.stderr, (
4209 'ERROR You are trying to send a job to a triggered bot. This type of'
4210 ' bot requires an\ninitial job from a parent (usually a builder). '
4211 'Instead send your job to the parent.\n'
4212 'Bot list: %s' % builders)
4213 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004214
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004215 patchset = cl.GetMostRecentPatchset()
4216 if patchset and patchset != cl.GetPatchset():
4217 print(
4218 '\nWARNING Mismatch between local config and server. Did a previous '
4219 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4220 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004221 if options.luci:
4222 trigger_luci_job(cl, masters, options)
4223 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004224 try:
4225 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4226 except BuildbucketResponseException as ex:
4227 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004228 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004229 except Exception as e:
4230 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4231 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
4232 e, stacktrace)
4233 return 1
4234 else:
4235 try:
4236 cl.RpcServer().trigger_distributed_try_jobs(
4237 cl.GetIssue(), patchset, options.name, options.clobber,
4238 options.revision, masters)
4239 except urllib2.HTTPError as e:
4240 if e.code == 404:
4241 print('404 from rietveld; '
4242 'did you mean to use "git try" instead of "git cl try"?')
4243 return 1
4244 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004245
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004246 for (master, builders) in sorted(masters.iteritems()):
4247 if master:
4248 print 'Master: %s' % master
4249 length = max(len(builder) for builder in builders)
4250 for builder in sorted(builders):
4251 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00004252 return 0
4253
4254
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004255def CMDtry_results(parser, args):
4256 group = optparse.OptionGroup(parser, "Try job results options")
4257 group.add_option(
4258 "-p", "--patchset", type=int, help="patchset number if not current.")
4259 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004260 "--print-master", action='store_true', help="print master name as well.")
4261 group.add_option(
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004262 "--color", action='store_true', default=setup_color.IS_TTY,
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004263 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004264 group.add_option(
4265 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4266 help="Host of buildbucket. The default host is %default.")
4267 parser.add_option_group(group)
4268 auth.add_auth_options(parser)
4269 options, args = parser.parse_args(args)
4270 if args:
4271 parser.error('Unrecognized args: %s' % ' '.join(args))
4272
4273 auth_config = auth.extract_auth_config_from_options(options)
4274 cl = Changelist(auth_config=auth_config)
4275 if not cl.GetIssue():
4276 parser.error('Need to upload first')
4277
4278 if not options.patchset:
4279 options.patchset = cl.GetMostRecentPatchset()
4280 if options.patchset and options.patchset != cl.GetPatchset():
4281 print(
4282 '\nWARNING Mismatch between local config and server. Did a previous '
4283 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4284 'Continuing using\npatchset %s.\n' % options.patchset)
4285 try:
4286 jobs = fetch_try_jobs(auth_config, cl, options)
4287 except BuildbucketResponseException as ex:
4288 print 'Buildbucket error: %s' % ex
4289 return 1
4290 except Exception as e:
4291 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4292 print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
4293 e, stacktrace)
4294 return 1
4295 print_tryjobs(options, jobs)
4296 return 0
4297
4298
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004299@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004300def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004301 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004302 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004303 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004304 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004305
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004306 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004307 if args:
4308 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004309 branch = cl.GetBranch()
4310 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004311 cl = Changelist()
4312 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004313
4314 # Clear configured merge-base, if there is one.
4315 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004316 else:
4317 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004318 return 0
4319
4320
thestig@chromium.org00858c82013-12-02 23:08:03 +00004321def CMDweb(parser, args):
4322 """Opens the current CL in the web browser."""
4323 _, args = parser.parse_args(args)
4324 if args:
4325 parser.error('Unrecognized args: %s' % ' '.join(args))
4326
4327 issue_url = Changelist().GetIssueURL()
4328 if not issue_url:
4329 print >> sys.stderr, 'ERROR No issue to open'
4330 return 1
4331
4332 webbrowser.open(issue_url)
4333 return 0
4334
4335
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004336def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004337 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004338 auth.add_auth_options(parser)
4339 options, args = parser.parse_args(args)
4340 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004341 if args:
4342 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004343 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004344 props = cl.GetIssueProperties()
4345 if props.get('private'):
4346 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004347 cl.SetFlag('commit', '1')
4348 return 0
4349
4350
groby@chromium.org411034a2013-02-26 15:12:01 +00004351def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004352 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004353 auth.add_auth_options(parser)
4354 options, args = parser.parse_args(args)
4355 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004356 if args:
4357 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004358 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00004359 # Ensure there actually is an issue to close.
4360 cl.GetDescription()
4361 cl.CloseIssue()
4362 return 0
4363
4364
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004365def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004366 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004367 auth.add_auth_options(parser)
4368 options, args = parser.parse_args(args)
4369 auth_config = auth.extract_auth_config_from_options(options)
4370 if args:
4371 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004372
4373 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004374 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004375 # Staged changes would be committed along with the patch from last
4376 # upload, hence counted toward the "last upload" side in the final
4377 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004378 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004379 return 1
4380
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004381 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004382 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004383 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004384 if not issue:
4385 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004386 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004387 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004388
4389 # Create a new branch based on the merge-base
4390 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004391 # Clear cached branch in cl object, to avoid overwriting original CL branch
4392 # properties.
4393 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004394 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004395 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004396 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004397 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004398 return rtn
4399
wychen@chromium.org06928532015-02-03 02:11:29 +00004400 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004401 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00004402 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004403 finally:
4404 RunGit(['checkout', '-q', branch])
4405 RunGit(['branch', '-D', TMP_BRANCH])
4406
4407 return 0
4408
4409
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004410def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004411 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004412 parser.add_option(
4413 '--no-color',
4414 action='store_true',
4415 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004416 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004417 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004418 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004419
4420 author = RunGit(['config', 'user.email']).strip() or None
4421
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004422 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004423
4424 if args:
4425 if len(args) > 1:
4426 parser.error('Unknown args')
4427 base_branch = args[0]
4428 else:
4429 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004430 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004431
4432 change = cl.GetChange(base_branch, None)
4433 return owners_finder.OwnersFinder(
4434 [f.LocalPath() for f in
4435 cl.GetChange(base_branch, None).AffectedFiles()],
4436 change.RepositoryRoot(), author,
4437 fopen=file, os_path=os.path, glob=glob.glob,
4438 disable_color=options.no_color).run()
4439
4440
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004441def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004442 """Generates a diff command."""
4443 # Generate diff for the current branch's changes.
4444 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4445 upstream_commit, '--' ]
4446
4447 if args:
4448 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004449 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004450 diff_cmd.append(arg)
4451 else:
4452 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004453
4454 return diff_cmd
4455
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004456def MatchingFileType(file_name, extensions):
4457 """Returns true if the file name ends with one of the given extensions."""
4458 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004459
enne@chromium.org555cfe42014-01-29 18:21:39 +00004460@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004461def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004462 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00004463 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004464 GN_EXTS = ['.gn', '.gni']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004465 parser.add_option('--full', action='store_true',
4466 help='Reformat the full content of all touched files')
4467 parser.add_option('--dry-run', action='store_true',
4468 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004469 parser.add_option('--python', action='store_true',
4470 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004471 parser.add_option('--diff', action='store_true',
4472 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004473 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004474
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004475 # git diff generates paths against the root of the repository. Change
4476 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004477 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004478 if rel_base_path:
4479 os.chdir(rel_base_path)
4480
digit@chromium.org29e47272013-05-17 17:01:46 +00004481 # Grab the merge-base commit, i.e. the upstream commit of the current
4482 # branch when it was created or the last time it was rebased. This is
4483 # to cover the case where the user may have called "git fetch origin",
4484 # moving the origin branch to a newer commit, but hasn't rebased yet.
4485 upstream_commit = None
4486 cl = Changelist()
4487 upstream_branch = cl.GetUpstreamBranch()
4488 if upstream_branch:
4489 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4490 upstream_commit = upstream_commit.strip()
4491
4492 if not upstream_commit:
4493 DieWithError('Could not find base commit for this branch. '
4494 'Are you in detached state?')
4495
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004496 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4497 diff_output = RunGit(changed_files_cmd)
4498 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004499 # Filter out files deleted by this CL
4500 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004501
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004502 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4503 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4504 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004505 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004506
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004507 top_dir = os.path.normpath(
4508 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
4509
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004510 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4511 # formatted. This is used to block during the presubmit.
4512 return_value = 0
4513
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004514 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004515 # Locate the clang-format binary in the checkout
4516 try:
4517 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
4518 except clang_format.NotFoundError, e:
4519 DieWithError(e)
4520
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004521 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004522 cmd = [clang_format_tool]
4523 if not opts.dry_run and not opts.diff:
4524 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004525 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004526 if opts.diff:
4527 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004528 else:
4529 env = os.environ.copy()
4530 env['PATH'] = str(os.path.dirname(clang_format_tool))
4531 try:
4532 script = clang_format.FindClangFormatScriptInChromiumTree(
4533 'clang-format-diff.py')
4534 except clang_format.NotFoundError, e:
4535 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004536
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004537 cmd = [sys.executable, script, '-p0']
4538 if not opts.dry_run and not opts.diff:
4539 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004540
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004541 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
4542 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004543
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004544 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
4545 if opts.diff:
4546 sys.stdout.write(stdout)
4547 if opts.dry_run and len(stdout) > 0:
4548 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004549
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004550 # Similar code to above, but using yapf on .py files rather than clang-format
4551 # on C/C++ files
4552 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004553 yapf_tool = gclient_utils.FindExecutable('yapf')
4554 if yapf_tool is None:
4555 DieWithError('yapf not found in PATH')
4556
4557 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004558 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004559 cmd = [yapf_tool]
4560 if not opts.dry_run and not opts.diff:
4561 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004562 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004563 if opts.diff:
4564 sys.stdout.write(stdout)
4565 else:
4566 # TODO(sbc): yapf --lines mode still has some issues.
4567 # https://github.com/google/yapf/issues/154
4568 DieWithError('--python currently only works with --full')
4569
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004570 # Dart's formatter does not have the nice property of only operating on
4571 # modified chunks, so hard code full.
4572 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004573 try:
4574 command = [dart_format.FindDartFmtToolInChromiumTree()]
4575 if not opts.dry_run and not opts.diff:
4576 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004577 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004578
ppi@chromium.org6593d932016-03-03 15:41:15 +00004579 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004580 if opts.dry_run and stdout:
4581 return_value = 2
4582 except dart_format.NotFoundError as e:
erikcorry@chromium.org3e445022015-12-17 09:07:26 +00004583 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
4584 'found in this checkout. Files in other languages are still ' +
4585 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004586
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004587 # Format GN build files. Always run on full build files for canonical form.
4588 if gn_diff_files:
4589 cmd = ['gn', 'format']
4590 if not opts.dry_run and not opts.diff:
4591 cmd.append('--in-place')
4592 for gn_diff_file in gn_diff_files:
4593 stdout = RunCommand(cmd + [gn_diff_file], cwd=top_dir)
4594 if opts.diff:
4595 sys.stdout.write(stdout)
4596
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004597 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004598
4599
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004600@subcommand.usage('<codereview url or issue id>')
4601def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004602 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004603 _, args = parser.parse_args(args)
4604
4605 if len(args) != 1:
4606 parser.print_help()
4607 return 1
4608
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004609 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00004610 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004611 parser.print_help()
4612 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00004613 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004614
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004615 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00004616 output = RunGit(['config', '--local', '--get-regexp',
4617 r'branch\..*\.%s' % issueprefix],
4618 error_ok=True)
4619 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004620 if issue == target_issue:
4621 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004622
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004623 branches = []
4624 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00004625 branches.extend(find_issues(cls.IssueSettingSuffix()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004626 if len(branches) == 0:
4627 print 'No branch found for issue %s.' % target_issue
4628 return 1
4629 if len(branches) == 1:
4630 RunGit(['checkout', branches[0]])
4631 else:
4632 print 'Multiple branches match issue %s:' % target_issue
4633 for i in range(len(branches)):
4634 print '%d: %s' % (i, branches[i])
4635 which = raw_input('Choose by index: ')
4636 try:
4637 RunGit(['checkout', branches[int(which)]])
4638 except (IndexError, ValueError):
4639 print 'Invalid selection, not checking out any branch.'
4640 return 1
4641
4642 return 0
4643
4644
maruel@chromium.org29404b52014-09-08 22:58:00 +00004645def CMDlol(parser, args):
4646 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00004647 print zlib.decompress(base64.b64decode(
4648 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
4649 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
4650 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
4651 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00004652 return 0
4653
4654
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004655class OptionParser(optparse.OptionParser):
4656 """Creates the option parse and add --verbose support."""
4657 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004658 optparse.OptionParser.__init__(
4659 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004660 self.add_option(
4661 '-v', '--verbose', action='count', default=0,
4662 help='Use 2 times for more debugging info')
4663
4664 def parse_args(self, args=None, values=None):
4665 options, args = optparse.OptionParser.parse_args(self, args, values)
4666 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
4667 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
4668 return options, args
4669
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004670
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004671def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00004672 if sys.hexversion < 0x02060000:
4673 print >> sys.stderr, (
4674 '\nYour python version %s is unsupported, please upgrade.\n' %
4675 sys.version.split(' ', 1)[0])
4676 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004677
maruel@chromium.orgddd59412011-11-30 14:20:38 +00004678 # Reload settings.
4679 global settings
4680 settings = Settings()
4681
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004682 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004683 dispatcher = subcommand.CommandDispatcher(__name__)
4684 try:
4685 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00004686 except auth.AuthenticationError as e:
4687 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004688 except urllib2.HTTPError, e:
4689 if e.code != 500:
4690 raise
4691 DieWithError(
4692 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
4693 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00004694 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004695
4696
4697if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004698 # These affect sys.stdout so do it outside of main() to simplify mocks in
4699 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00004700 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004701 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00004702 try:
4703 sys.exit(main(sys.argv[1:]))
4704 except KeyboardInterrupt:
4705 sys.stderr.write('interrupted\n')
4706 sys.exit(1)