blob: ea03734590eda9994bf8c6e4c0873b9b5bc03657 [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
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +0000881 Notes:
882 * Not safe for concurrent multi-{thread,process} use.
883 * Caches values from current branch. Therefore, re-use after branch change
884 with care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000885 """
886
887 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
888 """Create a new ChangeList instance.
889
890 If issue is given, the codereview must be given too.
891
892 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
893 Otherwise, it's decided based on current configuration of the local branch,
894 with default being 'rietveld' for backwards compatibility.
895 See _load_codereview_impl for more details.
896
897 **kwargs will be passed directly to codereview implementation.
898 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000899 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000900 global settings
901 if not settings:
902 # Happens when git_cl.py is used as a utility library.
903 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000904
905 if issue:
906 assert codereview, 'codereview must be known, if issue is known'
907
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000908 self.branchref = branchref
909 if self.branchref:
910 self.branch = ShortBranchName(self.branchref)
911 else:
912 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000913 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000914 self.lookedup_issue = False
915 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000916 self.has_description = False
917 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000918 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000919 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000920 self.cc = None
921 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000922 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000923
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000924 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000925 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000926 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000927 assert self._codereview_impl
928 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000929
930 def _load_codereview_impl(self, codereview=None, **kwargs):
931 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000932 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
933 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
934 self._codereview = codereview
935 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000936 return
937
938 # Automatic selection based on issue number set for a current branch.
939 # Rietveld takes precedence over Gerrit.
940 assert not self.issue
941 # Whether we find issue or not, we are doing the lookup.
942 self.lookedup_issue = True
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000943 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000944 setting = cls.IssueSetting(self.GetBranch())
945 issue = RunGit(['config', setting], error_ok=True).strip()
946 if issue:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000947 self._codereview = codereview
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000948 self._codereview_impl = cls(self, **kwargs)
949 self.issue = int(issue)
950 return
951
952 # No issue is set for this branch, so decide based on repo-wide settings.
953 return self._load_codereview_impl(
954 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
955 **kwargs)
956
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000957 def IsGerrit(self):
958 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000959
960 def GetCCList(self):
961 """Return the users cc'd on this CL.
962
963 Return is a string suitable for passing to gcl with the --cc flag.
964 """
965 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000966 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000967 more_cc = ','.join(self.watchers)
968 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
969 return self.cc
970
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000971 def GetCCListWithoutDefault(self):
972 """Return the users cc'd on this CL excluding default ones."""
973 if self.cc is None:
974 self.cc = ','.join(self.watchers)
975 return self.cc
976
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000977 def SetWatchers(self, watchers):
978 """Set the list of email addresses that should be cc'd based on the changed
979 files in this CL.
980 """
981 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000982
983 def GetBranch(self):
984 """Returns the short branch name, e.g. 'master'."""
985 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000986 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000987 if not branchref:
988 return None
989 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000990 self.branch = ShortBranchName(self.branchref)
991 return self.branch
992
993 def GetBranchRef(self):
994 """Returns the full branch name, e.g. 'refs/heads/master'."""
995 self.GetBranch() # Poke the lazy loader.
996 return self.branchref
997
tandrii@chromium.org534f67a2016-04-07 18:47:05 +0000998 def ClearBranch(self):
999 """Clears cached branch data of this object."""
1000 self.branch = self.branchref = None
1001
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001002 @staticmethod
1003 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001004 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001005 e.g. 'origin', 'refs/heads/master'
1006 """
1007 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001008 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
1009 error_ok=True).strip()
1010 if upstream_branch:
1011 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
1012 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001013 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1014 error_ok=True).strip()
1015 if upstream_branch:
1016 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001017 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001018 # Fall back on trying a git-svn upstream branch.
1019 if settings.GetIsGitSvn():
1020 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001021 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001022 # Else, try to guess the origin remote.
1023 remote_branches = RunGit(['branch', '-r']).split()
1024 if 'origin/master' in remote_branches:
1025 # Fall back on origin/master if it exits.
1026 remote = 'origin'
1027 upstream_branch = 'refs/heads/master'
1028 elif 'origin/trunk' in remote_branches:
1029 # Fall back on origin/trunk if it exists. Generally a shared
1030 # git-svn clone
1031 remote = 'origin'
1032 upstream_branch = 'refs/heads/trunk'
1033 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001034 DieWithError(
1035 'Unable to determine default branch to diff against.\n'
1036 'Either pass complete "git diff"-style arguments, like\n'
1037 ' git cl upload origin/master\n'
1038 'or verify this branch is set up to track another \n'
1039 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001040
1041 return remote, upstream_branch
1042
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001043 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001044 upstream_branch = self.GetUpstreamBranch()
1045 if not BranchExists(upstream_branch):
1046 DieWithError('The upstream for the current branch (%s) does not exist '
1047 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001048 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001049 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001050
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001051 def GetUpstreamBranch(self):
1052 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001053 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001054 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001055 upstream_branch = upstream_branch.replace('refs/heads/',
1056 'refs/remotes/%s/' % remote)
1057 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1058 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001059 self.upstream_branch = upstream_branch
1060 return self.upstream_branch
1061
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001062 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001063 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001064 remote, branch = None, self.GetBranch()
1065 seen_branches = set()
1066 while branch not in seen_branches:
1067 seen_branches.add(branch)
1068 remote, branch = self.FetchUpstreamTuple(branch)
1069 branch = ShortBranchName(branch)
1070 if remote != '.' or branch.startswith('refs/remotes'):
1071 break
1072 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001073 remotes = RunGit(['remote'], error_ok=True).split()
1074 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001075 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001076 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001077 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001078 logging.warning('Could not determine which remote this change is '
1079 'associated with, so defaulting to "%s". This may '
1080 'not be what you want. You may prevent this message '
1081 'by running "git svn info" as documented here: %s',
1082 self._remote,
1083 GIT_INSTRUCTIONS_URL)
1084 else:
1085 logging.warn('Could not determine which remote this change is '
1086 'associated with. You may prevent this message by '
1087 'running "git svn info" as documented here: %s',
1088 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001089 branch = 'HEAD'
1090 if branch.startswith('refs/remotes'):
1091 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001092 elif branch.startswith('refs/branch-heads/'):
1093 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001094 else:
1095 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001096 return self._remote
1097
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001098 def GitSanityChecks(self, upstream_git_obj):
1099 """Checks git repo status and ensures diff is from local commits."""
1100
sbc@chromium.org79706062015-01-14 21:18:12 +00001101 if upstream_git_obj is None:
1102 if self.GetBranch() is None:
1103 print >> sys.stderr, (
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00001104 'ERROR: unable to determine current branch (detached HEAD?)')
sbc@chromium.org79706062015-01-14 21:18:12 +00001105 else:
1106 print >> sys.stderr, (
1107 'ERROR: no upstream branch')
1108 return False
1109
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001110 # Verify the commit we're diffing against is in our current branch.
1111 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1112 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1113 if upstream_sha != common_ancestor:
1114 print >> sys.stderr, (
1115 'ERROR: %s is not in the current branch. You may need to rebase '
1116 'your tracking branch' % upstream_sha)
1117 return False
1118
1119 # List the commits inside the diff, and verify they are all local.
1120 commits_in_diff = RunGit(
1121 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1122 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1123 remote_branch = remote_branch.strip()
1124 if code != 0:
1125 _, remote_branch = self.GetRemoteBranch()
1126
1127 commits_in_remote = RunGit(
1128 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1129
1130 common_commits = set(commits_in_diff) & set(commits_in_remote)
1131 if common_commits:
1132 print >> sys.stderr, (
1133 'ERROR: Your diff contains %d commits already in %s.\n'
1134 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1135 'the diff. If you are using a custom git flow, you can override'
1136 ' the reference used for this check with "git config '
1137 'gitcl.remotebranch <git-ref>".' % (
1138 len(common_commits), remote_branch, upstream_git_obj))
1139 return False
1140 return True
1141
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001142 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001143 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001144
1145 Returns None if it is not set.
1146 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001147 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
1148 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001149
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001150 def GetGitSvnRemoteUrl(self):
1151 """Return the configured git-svn remote URL parsed from git svn info.
1152
1153 Returns None if it is not set.
1154 """
1155 # URL is dependent on the current directory.
1156 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1157 if data:
1158 keys = dict(line.split(': ', 1) for line in data.splitlines()
1159 if ': ' in line)
1160 return keys.get('URL', None)
1161 return None
1162
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001163 def GetRemoteUrl(self):
1164 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1165
1166 Returns None if there is no remote.
1167 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001168 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001169 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1170
1171 # If URL is pointing to a local directory, it is probably a git cache.
1172 if os.path.isdir(url):
1173 url = RunGit(['config', 'remote.%s.url' % remote],
1174 error_ok=True,
1175 cwd=url).strip()
1176 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001177
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001178 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001179 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001180 if self.issue is None and not self.lookedup_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001181 issue = RunGit(['config',
1182 self._codereview_impl.IssueSetting(self.GetBranch())],
1183 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001184 self.issue = int(issue) or None if issue else None
1185 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001186 return self.issue
1187
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001188 def GetIssueURL(self):
1189 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001190 issue = self.GetIssue()
1191 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001192 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001193 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001194
1195 def GetDescription(self, pretty=False):
1196 if not self.has_description:
1197 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001198 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001199 self.has_description = True
1200 if pretty:
1201 wrapper = textwrap.TextWrapper()
1202 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1203 return wrapper.fill(self.description)
1204 return self.description
1205
1206 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001207 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001208 if self.patchset is None and not self.lookedup_patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001209 patchset = RunGit(['config', self._codereview_impl.PatchsetSetting()],
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001210 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001211 self.patchset = int(patchset) or None if patchset else None
1212 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001213 return self.patchset
1214
1215 def SetPatchset(self, patchset):
1216 """Set this branch's patchset. If patchset=0, clears the patchset."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001217 patchset_setting = self._codereview_impl.PatchsetSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001218 if patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001219 RunGit(['config', patchset_setting, str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001220 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001221 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001222 RunGit(['config', '--unset', patchset_setting],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001223 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001224 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001225
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001226 def SetIssue(self, issue=None):
1227 """Set this branch's issue. If issue isn't given, clears the issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001228 issue_setting = self._codereview_impl.IssueSetting(self.GetBranch())
1229 codereview_setting = self._codereview_impl.GetCodereviewServerSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001230 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001231 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001232 RunGit(['config', issue_setting, str(issue)])
1233 codereview_server = self._codereview_impl.GetCodereviewServer()
1234 if codereview_server:
1235 RunGit(['config', codereview_setting, codereview_server])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001236 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +00001237 current_issue = self.GetIssue()
1238 if current_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001239 RunGit(['config', '--unset', issue_setting])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001240 self.issue = None
1241 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001242
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001243 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001244 if not self.GitSanityChecks(upstream_branch):
1245 DieWithError('\nGit sanity check failure')
1246
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001247 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001248 if not root:
1249 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001250 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001251
1252 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001253 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001254 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001255 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001256 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001257 except subprocess2.CalledProcessError:
1258 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001259 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001260 'This branch probably doesn\'t exist anymore. To reset the\n'
1261 'tracking branch, please run\n'
1262 ' git branch --set-upstream %s trunk\n'
1263 'replacing trunk with origin/master or the relevant branch') %
1264 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001265
maruel@chromium.org52424302012-08-29 15:14:30 +00001266 issue = self.GetIssue()
1267 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001268 if issue:
1269 description = self.GetDescription()
1270 else:
1271 # If the change was never uploaded, use the log messages of all commits
1272 # up to the branch point, as git cl upload will prefill the description
1273 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001274 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1275 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001276
1277 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001278 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001279 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001280 name,
1281 description,
1282 absroot,
1283 files,
1284 issue,
1285 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001286 author,
1287 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001288
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001289 def UpdateDescription(self, description):
1290 self.description = description
1291 return self._codereview_impl.UpdateDescriptionRemote(description)
1292
1293 def RunHook(self, committing, may_prompt, verbose, change):
1294 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1295 try:
1296 return presubmit_support.DoPresubmitChecks(change, committing,
1297 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1298 default_presubmit=None, may_prompt=may_prompt,
1299 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit())
1300 except presubmit_support.PresubmitFailure, e:
1301 DieWithError(
1302 ('%s\nMaybe your depot_tools is out of date?\n'
1303 'If all fails, contact maruel@') % e)
1304
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001305 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1306 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001307 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1308 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001309 else:
1310 # Assume url.
1311 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1312 urlparse.urlparse(issue_arg))
1313 if not parsed_issue_arg or not parsed_issue_arg.valid:
1314 DieWithError('Failed to parse issue argument "%s". '
1315 'Must be an issue number or a valid URL.' % issue_arg)
1316 return self._codereview_impl.CMDPatchWithParsedIssue(
1317 parsed_issue_arg, reject, nocommit, directory)
1318
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001319 def CMDUpload(self, options, git_diff_args, orig_args):
1320 """Uploads a change to codereview."""
1321 if git_diff_args:
1322 # TODO(ukai): is it ok for gerrit case?
1323 base_branch = git_diff_args[0]
1324 else:
1325 if self.GetBranch() is None:
1326 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1327
1328 # Default to diffing against common ancestor of upstream branch
1329 base_branch = self.GetCommonAncestorWithUpstream()
1330 git_diff_args = [base_branch, 'HEAD']
1331
1332 # Make sure authenticated to codereview before running potentially expensive
1333 # hooks. It is a fast, best efforts check. Codereview still can reject the
1334 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001335 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001336
1337 # Apply watchlists on upload.
1338 change = self.GetChange(base_branch, None)
1339 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1340 files = [f.LocalPath() for f in change.AffectedFiles()]
1341 if not options.bypass_watchlists:
1342 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1343
1344 if not options.bypass_hooks:
1345 if options.reviewers or options.tbr_owners:
1346 # Set the reviewer list now so that presubmit checks can access it.
1347 change_description = ChangeDescription(change.FullDescriptionText())
1348 change_description.update_reviewers(options.reviewers,
1349 options.tbr_owners,
1350 change)
1351 change.SetDescriptionText(change_description.description)
1352 hook_results = self.RunHook(committing=False,
1353 may_prompt=not options.force,
1354 verbose=options.verbose,
1355 change=change)
1356 if not hook_results.should_continue():
1357 return 1
1358 if not options.reviewers and hook_results.reviewers:
1359 options.reviewers = hook_results.reviewers.split(',')
1360
1361 if self.GetIssue():
1362 latest_patchset = self.GetMostRecentPatchset()
1363 local_patchset = self.GetPatchset()
1364 if (latest_patchset and local_patchset and
1365 local_patchset != latest_patchset):
1366 print ('The last upload made from this repository was patchset #%d but '
1367 'the most recent patchset on the server is #%d.'
1368 % (local_patchset, latest_patchset))
1369 print ('Uploading will still work, but if you\'ve uploaded to this '
1370 'issue from another machine or branch the patch you\'re '
1371 'uploading now might not include those changes.')
1372 ask_for_data('About to upload; enter to confirm.')
1373
1374 print_stats(options.similarity, options.find_copies, git_diff_args)
1375 ret = self.CMDUploadChange(options, git_diff_args, change)
1376 if not ret:
1377 git_set_branch_value('last-upload-hash',
1378 RunGit(['rev-parse', 'HEAD']).strip())
1379 # Run post upload hooks, if specified.
1380 if settings.GetRunPostUploadHook():
1381 presubmit_support.DoPostUploadExecuter(
1382 change,
1383 self,
1384 settings.GetRoot(),
1385 options.verbose,
1386 sys.stdout)
1387
1388 # Upload all dependencies if specified.
1389 if options.dependencies:
1390 print
1391 print '--dependencies has been specified.'
1392 print 'All dependent local branches will be re-uploaded.'
1393 print
1394 # Remove the dependencies flag from args so that we do not end up in a
1395 # loop.
1396 orig_args.remove('--dependencies')
1397 ret = upload_branch_deps(self, orig_args)
1398 return ret
1399
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001400 # Forward methods to codereview specific implementation.
1401
1402 def CloseIssue(self):
1403 return self._codereview_impl.CloseIssue()
1404
1405 def GetStatus(self):
1406 return self._codereview_impl.GetStatus()
1407
1408 def GetCodereviewServer(self):
1409 return self._codereview_impl.GetCodereviewServer()
1410
1411 def GetApprovingReviewers(self):
1412 return self._codereview_impl.GetApprovingReviewers()
1413
1414 def GetMostRecentPatchset(self):
1415 return self._codereview_impl.GetMostRecentPatchset()
1416
1417 def __getattr__(self, attr):
1418 # This is because lots of untested code accesses Rietveld-specific stuff
1419 # directly, and it's hard to fix for sure. So, just let it work, and fix
1420 # on a cases by case basis.
1421 return getattr(self._codereview_impl, attr)
1422
1423
1424class _ChangelistCodereviewBase(object):
1425 """Abstract base class encapsulating codereview specifics of a changelist."""
1426 def __init__(self, changelist):
1427 self._changelist = changelist # instance of Changelist
1428
1429 def __getattr__(self, attr):
1430 # Forward methods to changelist.
1431 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1432 # _RietveldChangelistImpl to avoid this hack?
1433 return getattr(self._changelist, attr)
1434
1435 def GetStatus(self):
1436 """Apply a rough heuristic to give a simple summary of an issue's review
1437 or CQ status, assuming adherence to a common workflow.
1438
1439 Returns None if no issue for this branch, or specific string keywords.
1440 """
1441 raise NotImplementedError()
1442
1443 def GetCodereviewServer(self):
1444 """Returns server URL without end slash, like "https://codereview.com"."""
1445 raise NotImplementedError()
1446
1447 def FetchDescription(self):
1448 """Fetches and returns description from the codereview server."""
1449 raise NotImplementedError()
1450
1451 def GetCodereviewServerSetting(self):
1452 """Returns git config setting for the codereview server."""
1453 raise NotImplementedError()
1454
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001455 @classmethod
1456 def IssueSetting(cls, branch):
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001457 return 'branch.%s.%s' % (branch, cls.IssueSettingSuffix())
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001458
1459 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001460 def IssueSettingSuffix(cls):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001461 """Returns name of git config setting which stores issue number for a given
1462 branch."""
1463 raise NotImplementedError()
1464
1465 def PatchsetSetting(self):
1466 """Returns name of git config setting which stores issue number."""
1467 raise NotImplementedError()
1468
1469 def GetRieveldObjForPresubmit(self):
1470 # This is an unfortunate Rietveld-embeddedness in presubmit.
1471 # For non-Rietveld codereviews, this probably should return a dummy object.
1472 raise NotImplementedError()
1473
1474 def UpdateDescriptionRemote(self, description):
1475 """Update the description on codereview site."""
1476 raise NotImplementedError()
1477
1478 def CloseIssue(self):
1479 """Closes the issue."""
1480 raise NotImplementedError()
1481
1482 def GetApprovingReviewers(self):
1483 """Returns a list of reviewers approving the change.
1484
1485 Note: not necessarily committers.
1486 """
1487 raise NotImplementedError()
1488
1489 def GetMostRecentPatchset(self):
1490 """Returns the most recent patchset number from the codereview site."""
1491 raise NotImplementedError()
1492
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001493 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1494 directory):
1495 """Fetches and applies the issue.
1496
1497 Arguments:
1498 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1499 reject: if True, reject the failed patch instead of switching to 3-way
1500 merge. Rietveld only.
1501 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1502 only.
1503 directory: switch to directory before applying the patch. Rietveld only.
1504 """
1505 raise NotImplementedError()
1506
1507 @staticmethod
1508 def ParseIssueURL(parsed_url):
1509 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1510 failed."""
1511 raise NotImplementedError()
1512
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001513 def EnsureAuthenticated(self, force):
1514 """Best effort check that user is authenticated with codereview server.
1515
1516 Arguments:
1517 force: whether to skip confirmation questions.
1518 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001519 raise NotImplementedError()
1520
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001521 def CMDUploadChange(self, options, args, change):
1522 """Uploads a change to codereview."""
1523 raise NotImplementedError()
1524
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001525
1526class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1527 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1528 super(_RietveldChangelistImpl, self).__init__(changelist)
1529 assert settings, 'must be initialized in _ChangelistCodereviewBase'
1530 settings.GetDefaultServerUrl()
1531
1532 self._rietveld_server = rietveld_server
1533 self._auth_config = auth_config
1534 self._props = None
1535 self._rpc_server = None
1536
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001537 def GetCodereviewServer(self):
1538 if not self._rietveld_server:
1539 # If we're on a branch then get the server potentially associated
1540 # with that branch.
1541 if self.GetIssue():
1542 rietveld_server_setting = self.GetCodereviewServerSetting()
1543 if rietveld_server_setting:
1544 self._rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1545 ['config', rietveld_server_setting], error_ok=True).strip())
1546 if not self._rietveld_server:
1547 self._rietveld_server = settings.GetDefaultServerUrl()
1548 return self._rietveld_server
1549
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001550 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001551 """Best effort check that user is authenticated with Rietveld server."""
1552 if self._auth_config.use_oauth2:
1553 authenticator = auth.get_authenticator_for_host(
1554 self.GetCodereviewServer(), self._auth_config)
1555 if not authenticator.has_cached_credentials():
1556 raise auth.LoginRequiredError(self.GetCodereviewServer())
1557
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001558 def FetchDescription(self):
1559 issue = self.GetIssue()
1560 assert issue
1561 try:
1562 return self.RpcServer().get_description(issue).strip()
1563 except urllib2.HTTPError as e:
1564 if e.code == 404:
1565 DieWithError(
1566 ('\nWhile fetching the description for issue %d, received a '
1567 '404 (not found)\n'
1568 'error. It is likely that you deleted this '
1569 'issue on the server. If this is the\n'
1570 'case, please run\n\n'
1571 ' git cl issue 0\n\n'
1572 'to clear the association with the deleted issue. Then run '
1573 'this command again.') % issue)
1574 else:
1575 DieWithError(
1576 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1577 except urllib2.URLError as e:
1578 print >> sys.stderr, (
1579 'Warning: Failed to retrieve CL description due to network '
1580 'failure.')
1581 return ''
1582
1583 def GetMostRecentPatchset(self):
1584 return self.GetIssueProperties()['patchsets'][-1]
1585
1586 def GetPatchSetDiff(self, issue, patchset):
1587 return self.RpcServer().get(
1588 '/download/issue%s_%s.diff' % (issue, patchset))
1589
1590 def GetIssueProperties(self):
1591 if self._props is None:
1592 issue = self.GetIssue()
1593 if not issue:
1594 self._props = {}
1595 else:
1596 self._props = self.RpcServer().get_issue_properties(issue, True)
1597 return self._props
1598
1599 def GetApprovingReviewers(self):
1600 return get_approving_reviewers(self.GetIssueProperties())
1601
1602 def AddComment(self, message):
1603 return self.RpcServer().add_comment(self.GetIssue(), message)
1604
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001605 def GetStatus(self):
1606 """Apply a rough heuristic to give a simple summary of an issue's review
1607 or CQ status, assuming adherence to a common workflow.
1608
1609 Returns None if no issue for this branch, or one of the following keywords:
1610 * 'error' - error from review tool (including deleted issues)
1611 * 'unsent' - not sent for review
1612 * 'waiting' - waiting for review
1613 * 'reply' - waiting for owner to reply to review
1614 * 'lgtm' - LGTM from at least one approved reviewer
1615 * 'commit' - in the commit queue
1616 * 'closed' - closed
1617 """
1618 if not self.GetIssue():
1619 return None
1620
1621 try:
1622 props = self.GetIssueProperties()
1623 except urllib2.HTTPError:
1624 return 'error'
1625
1626 if props.get('closed'):
1627 # Issue is closed.
1628 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001629 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001630 # Issue is in the commit queue.
1631 return 'commit'
1632
1633 try:
1634 reviewers = self.GetApprovingReviewers()
1635 except urllib2.HTTPError:
1636 return 'error'
1637
1638 if reviewers:
1639 # Was LGTM'ed.
1640 return 'lgtm'
1641
1642 messages = props.get('messages') or []
1643
1644 if not messages:
1645 # No message was sent.
1646 return 'unsent'
1647 if messages[-1]['sender'] != props.get('owner_email'):
1648 # Non-LGTM reply from non-owner
1649 return 'reply'
1650 return 'waiting'
1651
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001652 def UpdateDescriptionRemote(self, description):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001653 return self.RpcServer().update_description(
1654 self.GetIssue(), self.description)
1655
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001656 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001657 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001658
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001659 def SetFlag(self, flag, value):
1660 """Patchset must match."""
1661 if not self.GetPatchset():
1662 DieWithError('The patchset needs to match. Send another patchset.')
1663 try:
1664 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001665 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001666 except urllib2.HTTPError, e:
1667 if e.code == 404:
1668 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1669 if e.code == 403:
1670 DieWithError(
1671 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1672 'match?') % (self.GetIssue(), self.GetPatchset()))
1673 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001674
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001675 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001676 """Returns an upload.RpcServer() to access this review's rietveld instance.
1677 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001678 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001679 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001680 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001681 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001682 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001683
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001684 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001685 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001686 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001687
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001688 def PatchsetSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001689 """Return the git setting that stores this change's most recent patchset."""
1690 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1691
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001692 def GetCodereviewServerSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001693 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001694 branch = self.GetBranch()
1695 if branch:
1696 return 'branch.%s.rietveldserver' % branch
1697 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001698
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001699 def GetRieveldObjForPresubmit(self):
1700 return self.RpcServer()
1701
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001702 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1703 directory):
1704 # TODO(maruel): Use apply_issue.py
1705
1706 # PatchIssue should never be called with a dirty tree. It is up to the
1707 # caller to check this, but just in case we assert here since the
1708 # consequences of the caller not checking this could be dire.
1709 assert(not git_common.is_dirty_git_tree('apply'))
1710 assert(parsed_issue_arg.valid)
1711 self._changelist.issue = parsed_issue_arg.issue
1712 if parsed_issue_arg.hostname:
1713 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1714
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001715 if (isinstance(parsed_issue_arg, _RietveldParsedIssueNumberArgument) and
1716 parsed_issue_arg.patch_url):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001717 assert parsed_issue_arg.patchset
1718 patchset = parsed_issue_arg.patchset
1719 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1720 else:
1721 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1722 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1723
1724 # Switch up to the top-level directory, if necessary, in preparation for
1725 # applying the patch.
1726 top = settings.GetRelativeRoot()
1727 if top:
1728 os.chdir(top)
1729
1730 # Git patches have a/ at the beginning of source paths. We strip that out
1731 # with a sed script rather than the -p flag to patch so we can feed either
1732 # Git or svn-style patches into the same apply command.
1733 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1734 try:
1735 patch_data = subprocess2.check_output(
1736 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1737 except subprocess2.CalledProcessError:
1738 DieWithError('Git patch mungling failed.')
1739 logging.info(patch_data)
1740
1741 # We use "git apply" to apply the patch instead of "patch" so that we can
1742 # pick up file adds.
1743 # The --index flag means: also insert into the index (so we catch adds).
1744 cmd = ['git', 'apply', '--index', '-p0']
1745 if directory:
1746 cmd.extend(('--directory', directory))
1747 if reject:
1748 cmd.append('--reject')
1749 elif IsGitVersionAtLeast('1.7.12'):
1750 cmd.append('--3way')
1751 try:
1752 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
1753 stdin=patch_data, stdout=subprocess2.VOID)
1754 except subprocess2.CalledProcessError:
1755 print 'Failed to apply the patch'
1756 return 1
1757
1758 # If we had an issue, commit the current state and register the issue.
1759 if not nocommit:
1760 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
1761 'patch from issue %(i)s at patchset '
1762 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
1763 % {'i': self.GetIssue(), 'p': patchset})])
1764 self.SetIssue(self.GetIssue())
1765 self.SetPatchset(patchset)
1766 print "Committed patch locally."
1767 else:
1768 print "Patch applied to index."
1769 return 0
1770
1771 @staticmethod
1772 def ParseIssueURL(parsed_url):
1773 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1774 return None
1775 # Typical url: https://domain/<issue_number>[/[other]]
1776 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
1777 if match:
1778 return _RietveldParsedIssueNumberArgument(
1779 issue=int(match.group(1)),
1780 hostname=parsed_url.netloc)
1781 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
1782 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
1783 if match:
1784 return _RietveldParsedIssueNumberArgument(
1785 issue=int(match.group(1)),
1786 patchset=int(match.group(2)),
1787 hostname=parsed_url.netloc,
1788 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
1789 return None
1790
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001791 def CMDUploadChange(self, options, args, change):
1792 """Upload the patch to Rietveld."""
1793 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1794 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001795 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
1796 if options.emulate_svn_auto_props:
1797 upload_args.append('--emulate_svn_auto_props')
1798
1799 change_desc = None
1800
1801 if options.email is not None:
1802 upload_args.extend(['--email', options.email])
1803
1804 if self.GetIssue():
1805 if options.title:
1806 upload_args.extend(['--title', options.title])
1807 if options.message:
1808 upload_args.extend(['--message', options.message])
1809 upload_args.extend(['--issue', str(self.GetIssue())])
1810 print ('This branch is associated with issue %s. '
1811 'Adding patch to that issue.' % self.GetIssue())
1812 else:
1813 if options.title:
1814 upload_args.extend(['--title', options.title])
1815 message = (options.title or options.message or
1816 CreateDescriptionFromLog(args))
1817 change_desc = ChangeDescription(message)
1818 if options.reviewers or options.tbr_owners:
1819 change_desc.update_reviewers(options.reviewers,
1820 options.tbr_owners,
1821 change)
1822 if not options.force:
1823 change_desc.prompt()
1824
1825 if not change_desc.description:
1826 print "Description is empty; aborting."
1827 return 1
1828
1829 upload_args.extend(['--message', change_desc.description])
1830 if change_desc.get_reviewers():
1831 upload_args.append('--reviewers=%s' % ','.join(
1832 change_desc.get_reviewers()))
1833 if options.send_mail:
1834 if not change_desc.get_reviewers():
1835 DieWithError("Must specify reviewers to send email.")
1836 upload_args.append('--send_mail')
1837
1838 # We check this before applying rietveld.private assuming that in
1839 # rietveld.cc only addresses which we can send private CLs to are listed
1840 # if rietveld.private is set, and so we should ignore rietveld.cc only
1841 # when --private is specified explicitly on the command line.
1842 if options.private:
1843 logging.warn('rietveld.cc is ignored since private flag is specified. '
1844 'You need to review and add them manually if necessary.')
1845 cc = self.GetCCListWithoutDefault()
1846 else:
1847 cc = self.GetCCList()
1848 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
1849 if cc:
1850 upload_args.extend(['--cc', cc])
1851
1852 if options.private or settings.GetDefaultPrivateFlag() == "True":
1853 upload_args.append('--private')
1854
1855 upload_args.extend(['--git_similarity', str(options.similarity)])
1856 if not options.find_copies:
1857 upload_args.extend(['--git_no_find_copies'])
1858
1859 # Include the upstream repo's URL in the change -- this is useful for
1860 # projects that have their source spread across multiple repos.
1861 remote_url = self.GetGitBaseUrlFromConfig()
1862 if not remote_url:
1863 if settings.GetIsGitSvn():
1864 remote_url = self.GetGitSvnRemoteUrl()
1865 else:
1866 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
1867 remote_url = '%s@%s' % (self.GetRemoteUrl(),
1868 self.GetUpstreamBranch().split('/')[-1])
1869 if remote_url:
1870 upload_args.extend(['--base_url', remote_url])
1871 remote, remote_branch = self.GetRemoteBranch()
1872 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
1873 settings.GetPendingRefPrefix())
1874 if target_ref:
1875 upload_args.extend(['--target_ref', target_ref])
1876
1877 # Look for dependent patchsets. See crbug.com/480453 for more details.
1878 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
1879 upstream_branch = ShortBranchName(upstream_branch)
1880 if remote is '.':
1881 # A local branch is being tracked.
1882 local_branch = ShortBranchName(upstream_branch)
1883 if settings.GetIsSkipDependencyUpload(local_branch):
1884 print
1885 print ('Skipping dependency patchset upload because git config '
1886 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
1887 print
1888 else:
1889 auth_config = auth.extract_auth_config_from_options(options)
1890 branch_cl = Changelist(branchref=local_branch,
1891 auth_config=auth_config)
1892 branch_cl_issue_url = branch_cl.GetIssueURL()
1893 branch_cl_issue = branch_cl.GetIssue()
1894 branch_cl_patchset = branch_cl.GetPatchset()
1895 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
1896 upload_args.extend(
1897 ['--depends_on_patchset', '%s:%s' % (
1898 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001899 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001900 '\n'
1901 'The current branch (%s) is tracking a local branch (%s) with '
1902 'an associated CL.\n'
1903 'Adding %s/#ps%s as a dependency patchset.\n'
1904 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
1905 branch_cl_patchset))
1906
1907 project = settings.GetProject()
1908 if project:
1909 upload_args.extend(['--project', project])
1910
1911 if options.cq_dry_run:
1912 upload_args.extend(['--cq_dry_run'])
1913
1914 try:
1915 upload_args = ['upload'] + upload_args + args
1916 logging.info('upload.RealMain(%s)', upload_args)
1917 issue, patchset = upload.RealMain(upload_args)
1918 issue = int(issue)
1919 patchset = int(patchset)
1920 except KeyboardInterrupt:
1921 sys.exit(1)
1922 except:
1923 # If we got an exception after the user typed a description for their
1924 # change, back up the description before re-raising.
1925 if change_desc:
1926 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1927 print('\nGot exception while uploading -- saving description to %s\n' %
1928 backup_path)
1929 backup_file = open(backup_path, 'w')
1930 backup_file.write(change_desc.description)
1931 backup_file.close()
1932 raise
1933
1934 if not self.GetIssue():
1935 self.SetIssue(issue)
1936 self.SetPatchset(patchset)
1937
1938 if options.use_commit_queue:
1939 self.SetFlag('commit', '1')
1940 return 0
1941
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001942
1943class _GerritChangelistImpl(_ChangelistCodereviewBase):
1944 def __init__(self, changelist, auth_config=None):
1945 # auth_config is Rietveld thing, kept here to preserve interface only.
1946 super(_GerritChangelistImpl, self).__init__(changelist)
1947 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001948 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001949 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001950 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001951
1952 def _GetGerritHost(self):
1953 # Lazy load of configs.
1954 self.GetCodereviewServer()
1955 return self._gerrit_host
1956
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001957 def _GetGitHost(self):
1958 """Returns git host to be used when uploading change to Gerrit."""
1959 return urlparse.urlparse(self.GetRemoteUrl()).netloc
1960
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001961 def GetCodereviewServer(self):
1962 if not self._gerrit_server:
1963 # If we're on a branch then get the server potentially associated
1964 # with that branch.
1965 if self.GetIssue():
1966 gerrit_server_setting = self.GetCodereviewServerSetting()
1967 if gerrit_server_setting:
1968 self._gerrit_server = RunGit(['config', gerrit_server_setting],
1969 error_ok=True).strip()
1970 if self._gerrit_server:
1971 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
1972 if not self._gerrit_server:
1973 # We assume repo to be hosted on Gerrit, and hence Gerrit server
1974 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001975 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001976 parts[0] = parts[0] + '-review'
1977 self._gerrit_host = '.'.join(parts)
1978 self._gerrit_server = 'https://%s' % self._gerrit_host
1979 return self._gerrit_server
1980
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001981 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001982 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001983 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001984
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001985 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001986 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001987 # Lazy-loader to identify Gerrit and Git hosts.
1988 if gerrit_util.GceAuthenticator.is_gce():
1989 return
1990 self.GetCodereviewServer()
1991 git_host = self._GetGitHost()
1992 assert self._gerrit_server and self._gerrit_host
1993 cookie_auth = gerrit_util.CookiesAuthenticator()
1994
1995 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
1996 git_auth = cookie_auth.get_auth_header(git_host)
1997 if gerrit_auth and git_auth:
1998 if gerrit_auth == git_auth:
1999 return
2000 print((
2001 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2002 ' Check your %s or %s file for credentials of hosts:\n'
2003 ' %s\n'
2004 ' %s\n'
2005 ' %s') %
2006 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2007 git_host, self._gerrit_host,
2008 cookie_auth.get_new_password_message(git_host)))
2009 if not force:
2010 ask_for_data('If you know what you are doing, press Enter to continue, '
2011 'Ctrl+C to abort.')
2012 return
2013 else:
2014 missing = (
2015 [] if gerrit_auth else [self._gerrit_host] +
2016 [] if git_auth else [git_host])
2017 DieWithError('Credentials for the following hosts are required:\n'
2018 ' %s\n'
2019 'These are read from %s (or legacy %s)\n'
2020 '%s' % (
2021 '\n '.join(missing),
2022 cookie_auth.get_gitcookies_path(),
2023 cookie_auth.get_netrc_path(),
2024 cookie_auth.get_new_password_message(git_host)))
2025
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002026
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002027 def PatchsetSetting(self):
2028 """Return the git setting that stores this change's most recent patchset."""
2029 return 'branch.%s.gerritpatchset' % self.GetBranch()
2030
2031 def GetCodereviewServerSetting(self):
2032 """Returns the git setting that stores this change's Gerrit server."""
2033 branch = self.GetBranch()
2034 if branch:
2035 return 'branch.%s.gerritserver' % branch
2036 return None
2037
2038 def GetRieveldObjForPresubmit(self):
2039 class ThisIsNotRietveldIssue(object):
2040 def __nonzero__(self):
2041 # This is a hack to make presubmit_support think that rietveld is not
2042 # defined, yet still ensure that calls directly result in a decent
2043 # exception message below.
2044 return False
2045
2046 def __getattr__(self, attr):
2047 print(
2048 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2049 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2050 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2051 'or use Rietveld for codereview.\n'
2052 'See also http://crbug.com/579160.' % attr)
2053 raise NotImplementedError()
2054 return ThisIsNotRietveldIssue()
2055
2056 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002057 """Apply a rough heuristic to give a simple summary of an issue's review
2058 or CQ status, assuming adherence to a common workflow.
2059
2060 Returns None if no issue for this branch, or one of the following keywords:
2061 * 'error' - error from review tool (including deleted issues)
2062 * 'unsent' - no reviewers added
2063 * 'waiting' - waiting for review
2064 * 'reply' - waiting for owner to reply to review
2065 * 'not lgtm' - Code-Review -2 from at least one approved reviewer
2066 * 'lgtm' - Code-Review +2 from at least one approved reviewer
2067 * 'commit' - in the commit queue
2068 * 'closed' - abandoned
2069 """
2070 if not self.GetIssue():
2071 return None
2072
2073 try:
2074 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
2075 except httplib.HTTPException:
2076 return 'error'
2077
2078 if data['status'] == 'ABANDONED':
2079 return 'closed'
2080
2081 cq_label = data['labels'].get('Commit-Queue', {})
2082 if cq_label:
2083 # Vote value is a stringified integer, which we expect from 0 to 2.
2084 vote_value = cq_label.get('value', '0')
2085 vote_text = cq_label.get('values', {}).get(vote_value, '')
2086 if vote_text.lower() == 'commit':
2087 return 'commit'
2088
2089 lgtm_label = data['labels'].get('Code-Review', {})
2090 if lgtm_label:
2091 if 'rejected' in lgtm_label:
2092 return 'not lgtm'
2093 if 'approved' in lgtm_label:
2094 return 'lgtm'
2095
2096 if not data.get('reviewers', {}).get('REVIEWER', []):
2097 return 'unsent'
2098
2099 messages = data.get('messages', [])
2100 if messages:
2101 owner = data['owner'].get('_account_id')
2102 last_message_author = messages[-1].get('author', {}).get('_account_id')
2103 if owner != last_message_author:
2104 # Some reply from non-owner.
2105 return 'reply'
2106
2107 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002108
2109 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002110 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002111 return data['revisions'][data['current_revision']]['_number']
2112
2113 def FetchDescription(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002114 data = self._GetChangeDetail(['COMMIT_FOOTERS', 'CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002115 return data['revisions'][data['current_revision']]['commit_with_footers']
2116
2117 def UpdateDescriptionRemote(self, description):
2118 # TODO(tandrii)
2119 raise NotImplementedError()
2120
2121 def CloseIssue(self):
2122 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2123
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002124 def SubmitIssue(self, wait_for_merge=True):
2125 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2126 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002127
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002128 def _GetChangeDetail(self, options=None, issue=None):
2129 options = options or []
2130 issue = issue or self.GetIssue()
2131 assert issue, 'issue required to query Gerrit'
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002132 return gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2133 options)
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002134
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002135 def CMDLand(self, force, bypass_hooks, verbose):
2136 if git_common.is_dirty_git_tree('land'):
2137 return 1
2138 differs = True
2139 last_upload = RunGit(['config',
2140 'branch.%s.gerritsquashhash' % self.GetBranch()],
2141 error_ok=True).strip()
2142 # Note: git diff outputs nothing if there is no diff.
2143 if not last_upload or RunGit(['diff', last_upload]).strip():
2144 print('WARNING: some changes from local branch haven\'t been uploaded')
2145 else:
2146 detail = self._GetChangeDetail(['CURRENT_REVISION'])
2147 if detail['current_revision'] == last_upload:
2148 differs = False
2149 else:
2150 print('WARNING: local branch contents differ from latest uploaded '
2151 'patchset')
2152 if differs:
2153 if not force:
2154 ask_for_data(
2155 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2156 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2157 elif not bypass_hooks:
2158 hook_results = self.RunHook(
2159 committing=True,
2160 may_prompt=not force,
2161 verbose=verbose,
2162 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2163 if not hook_results.should_continue():
2164 return 1
2165
2166 self.SubmitIssue(wait_for_merge=True)
2167 print('Issue %s has been submitted.' % self.GetIssueURL())
2168 return 0
2169
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002170 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2171 directory):
2172 assert not reject
2173 assert not nocommit
2174 assert not directory
2175 assert parsed_issue_arg.valid
2176
2177 self._changelist.issue = parsed_issue_arg.issue
2178
2179 if parsed_issue_arg.hostname:
2180 self._gerrit_host = parsed_issue_arg.hostname
2181 self._gerrit_server = 'https://%s' % self._gerrit_host
2182
2183 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2184
2185 if not parsed_issue_arg.patchset:
2186 # Use current revision by default.
2187 revision_info = detail['revisions'][detail['current_revision']]
2188 patchset = int(revision_info['_number'])
2189 else:
2190 patchset = parsed_issue_arg.patchset
2191 for revision_info in detail['revisions'].itervalues():
2192 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2193 break
2194 else:
2195 DieWithError('Couldn\'t find patchset %i in issue %i' %
2196 (parsed_issue_arg.patchset, self.GetIssue()))
2197
2198 fetch_info = revision_info['fetch']['http']
2199 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2200 RunGit(['cherry-pick', 'FETCH_HEAD'])
2201 self.SetIssue(self.GetIssue())
2202 self.SetPatchset(patchset)
2203 print('Committed patch for issue %i pathset %i locally' %
2204 (self.GetIssue(), self.GetPatchset()))
2205 return 0
2206
2207 @staticmethod
2208 def ParseIssueURL(parsed_url):
2209 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2210 return None
2211 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2212 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2213 # Short urls like https://domain/<issue_number> can be used, but don't allow
2214 # specifying the patchset (you'd 404), but we allow that here.
2215 if parsed_url.path == '/':
2216 part = parsed_url.fragment
2217 else:
2218 part = parsed_url.path
2219 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2220 if match:
2221 return _ParsedIssueNumberArgument(
2222 issue=int(match.group(2)),
2223 patchset=int(match.group(4)) if match.group(4) else None,
2224 hostname=parsed_url.netloc)
2225 return None
2226
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002227 def CMDUploadChange(self, options, args, change):
2228 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002229 if options.squash and options.no_squash:
2230 DieWithError('Can only use one of --squash or --no-squash')
2231 options.squash = ((settings.GetSquashGerritUploads() or options.squash) and
2232 not options.no_squash)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002233 # We assume the remote called "origin" is the one we want.
2234 # It is probably not worthwhile to support different workflows.
2235 gerrit_remote = 'origin'
2236
2237 remote, remote_branch = self.GetRemoteBranch()
2238 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2239 pending_prefix='')
2240
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002241 if options.squash:
2242 if not self.GetIssue():
2243 # TODO(tandrii): deperecate this after 2016Q2. Backwards compatibility
2244 # with shadow branch, which used to contain change-id for a given
2245 # branch, using which we can fetch actual issue number and set it as the
2246 # property of the branch, which is the new way.
2247 message = RunGitSilent([
2248 'show', '--format=%B', '-s',
2249 'refs/heads/git_cl_uploads/%s' % self.GetBranch()])
2250 if message:
2251 change_ids = git_footers.get_footer_change_id(message.strip())
2252 if change_ids and len(change_ids) == 1:
2253 details = self._GetChangeDetail(issue=change_ids[0])
2254 if details:
2255 print('WARNING: found old upload in branch git_cl_uploads/%s '
2256 'corresponding to issue %s' %
2257 (self.GetBranch(), details['_number']))
2258 self.SetIssue(details['_number'])
2259 if not self.GetIssue():
2260 DieWithError(
2261 '\n' # For readability of the blob below.
2262 'Found old upload in branch git_cl_uploads/%s, '
2263 'but failed to find corresponding Gerrit issue.\n'
2264 'If you know the issue number, set it manually first:\n'
2265 ' git cl issue 123456\n'
2266 'If you intended to upload this CL as new issue, '
2267 'just delete or rename the old upload branch:\n'
2268 ' git rename-branch git_cl_uploads/%s old_upload-%s\n'
2269 'After that, please run git cl upload again.' %
2270 tuple([self.GetBranch()] * 3))
2271 # End of backwards compatability.
2272
2273 if self.GetIssue():
2274 # Try to get the message from a previous upload.
2275 message = self.GetDescription()
2276 if not message:
2277 DieWithError(
2278 'failed to fetch description from current Gerrit issue %d\n'
2279 '%s' % (self.GetIssue(), self.GetIssueURL()))
2280 change_id = self._GetChangeDetail()['change_id']
2281 while True:
2282 footer_change_ids = git_footers.get_footer_change_id(message)
2283 if footer_change_ids == [change_id]:
2284 break
2285 if not footer_change_ids:
2286 message = git_footers.add_footer_change_id(message, change_id)
2287 print('WARNING: appended missing Change-Id to issue description')
2288 continue
2289 # There is already a valid footer but with different or several ids.
2290 # Doing this automatically is non-trivial as we don't want to lose
2291 # existing other footers, yet we want to append just 1 desired
2292 # Change-Id. Thus, just create a new footer, but let user verify the
2293 # new description.
2294 message = '%s\n\nChange-Id: %s' % (message, change_id)
2295 print(
2296 'WARNING: issue %s has Change-Id footer(s):\n'
2297 ' %s\n'
2298 'but issue has Change-Id %s, according to Gerrit.\n'
2299 'Please, check the proposed correction to the description, '
2300 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2301 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2302 change_id))
2303 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2304 if not options.force:
2305 change_desc = ChangeDescription(message)
2306 change_desc.prompt()
2307 message = change_desc.description
2308 if not message:
2309 DieWithError("Description is empty. Aborting...")
2310 # Continue the while loop.
2311 # Sanity check of this code - we should end up with proper message
2312 # footer.
2313 assert [change_id] == git_footers.get_footer_change_id(message)
2314 change_desc = ChangeDescription(message)
2315 else:
2316 change_desc = ChangeDescription(
2317 options.message or CreateDescriptionFromLog(args))
2318 if not options.force:
2319 change_desc.prompt()
2320 if not change_desc.description:
2321 DieWithError("Description is empty. Aborting...")
2322 message = change_desc.description
2323 change_ids = git_footers.get_footer_change_id(message)
2324 if len(change_ids) > 1:
2325 DieWithError('too many Change-Id footers, at most 1 allowed.')
2326 if not change_ids:
2327 # Generate the Change-Id automatically.
2328 message = git_footers.add_footer_change_id(
2329 message, GenerateGerritChangeId(message))
2330 change_desc.set_description(message)
2331 change_ids = git_footers.get_footer_change_id(message)
2332 assert len(change_ids) == 1
2333 change_id = change_ids[0]
2334
2335 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2336 if remote is '.':
2337 # If our upstream branch is local, we base our squashed commit on its
2338 # squashed version.
2339 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2340 # Check the squashed hash of the parent.
2341 parent = RunGit(['config',
2342 'branch.%s.gerritsquashhash' % upstream_branch_name],
2343 error_ok=True).strip()
2344 # Verify that the upstream branch has been uploaded too, otherwise
2345 # Gerrit will create additional CLs when uploading.
2346 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2347 RunGitSilent(['rev-parse', parent + ':'])):
2348 # TODO(tandrii): remove "old depot_tools" part on April 12, 2016.
2349 DieWithError(
2350 'Upload upstream branch %s first.\n'
2351 'Note: maybe you\'ve uploaded it with --no-squash or with an old '
2352 'version of depot_tools. If so, then re-upload it with:\n'
2353 ' git cl upload --squash\n' % upstream_branch_name)
2354 else:
2355 parent = self.GetCommonAncestorWithUpstream()
2356
2357 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2358 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2359 '-m', message]).strip()
2360 else:
2361 change_desc = ChangeDescription(
2362 options.message or CreateDescriptionFromLog(args))
2363 if not change_desc.description:
2364 DieWithError("Description is empty. Aborting...")
2365
2366 if not git_footers.get_footer_change_id(change_desc.description):
2367 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002368 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2369 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002370 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
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002389 # Extra options that can be specified at push time. Doc:
2390 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2391 refspec_opts = []
2392 if options.title:
2393 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2394 # reverse on its side.
2395 if '_' in options.title:
2396 print('WARNING: underscores in title will be converted to spaces.')
2397 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2398
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002399 cc = self.GetCCList().split(',')
2400 if options.cc:
2401 cc.extend(options.cc)
2402 cc = filter(None, cc)
2403 if cc:
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002404 refspec_opts.extend('cc=' + email.strip() for email in cc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002405
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002406 if change_desc.get_reviewers():
2407 refspec_opts.extend('r=' + email.strip()
2408 for email in change_desc.get_reviewers())
2409
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002410
2411 refspec_suffix = ''
2412 if refspec_opts:
2413 refspec_suffix = '%' + ','.join(refspec_opts)
2414 assert ' ' not in refspec_suffix, (
2415 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002416 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002417
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002418 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002419 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002420 print_stdout=True,
2421 # Flush after every line: useful for seeing progress when running as
2422 # recipe.
2423 filter_fn=lambda _: sys.stdout.flush())
2424
2425 if options.squash:
2426 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2427 change_numbers = [m.group(1)
2428 for m in map(regex.match, push_stdout.splitlines())
2429 if m]
2430 if len(change_numbers) != 1:
2431 DieWithError(
2432 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2433 'Change-Id: %s') % (len(change_numbers), change_id))
2434 self.SetIssue(change_numbers[0])
2435 RunGit(['config', 'branch.%s.gerritsquashhash' % self.GetBranch(),
2436 ref_to_push])
2437 return 0
2438
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002439 def _AddChangeIdToCommitMessage(self, options, args):
2440 """Re-commits using the current message, assumes the commit hook is in
2441 place.
2442 """
2443 log_desc = options.message or CreateDescriptionFromLog(args)
2444 git_command = ['commit', '--amend', '-m', log_desc]
2445 RunGit(git_command)
2446 new_log_desc = CreateDescriptionFromLog(args)
2447 if git_footers.get_footer_change_id(new_log_desc):
2448 print 'git-cl: Added Change-Id to commit message.'
2449 return new_log_desc
2450 else:
2451 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002452
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002453
2454_CODEREVIEW_IMPLEMENTATIONS = {
2455 'rietveld': _RietveldChangelistImpl,
2456 'gerrit': _GerritChangelistImpl,
2457}
2458
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002459
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002460class ChangeDescription(object):
2461 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002462 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002463 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002464
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002465 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002466 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002467
agable@chromium.org42c20792013-09-12 17:34:49 +00002468 @property # www.logilab.org/ticket/89786
2469 def description(self): # pylint: disable=E0202
2470 return '\n'.join(self._description_lines)
2471
2472 def set_description(self, desc):
2473 if isinstance(desc, basestring):
2474 lines = desc.splitlines()
2475 else:
2476 lines = [line.rstrip() for line in desc]
2477 while lines and not lines[0]:
2478 lines.pop(0)
2479 while lines and not lines[-1]:
2480 lines.pop(-1)
2481 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002482
piman@chromium.org336f9122014-09-04 02:16:55 +00002483 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002484 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002485 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002486 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002487 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002488 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002489
agable@chromium.org42c20792013-09-12 17:34:49 +00002490 # Get the set of R= and TBR= lines and remove them from the desciption.
2491 regexp = re.compile(self.R_LINE)
2492 matches = [regexp.match(line) for line in self._description_lines]
2493 new_desc = [l for i, l in enumerate(self._description_lines)
2494 if not matches[i]]
2495 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002496
agable@chromium.org42c20792013-09-12 17:34:49 +00002497 # Construct new unified R= and TBR= lines.
2498 r_names = []
2499 tbr_names = []
2500 for match in matches:
2501 if not match:
2502 continue
2503 people = cleanup_list([match.group(2).strip()])
2504 if match.group(1) == 'TBR':
2505 tbr_names.extend(people)
2506 else:
2507 r_names.extend(people)
2508 for name in r_names:
2509 if name not in reviewers:
2510 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002511 if add_owners_tbr:
2512 owners_db = owners.Database(change.RepositoryRoot(),
2513 fopen=file, os_path=os.path, glob=glob.glob)
2514 all_reviewers = set(tbr_names + reviewers)
2515 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2516 all_reviewers)
2517 tbr_names.extend(owners_db.reviewers_for(missing_files,
2518 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002519 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2520 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2521
2522 # Put the new lines in the description where the old first R= line was.
2523 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2524 if 0 <= line_loc < len(self._description_lines):
2525 if new_tbr_line:
2526 self._description_lines.insert(line_loc, new_tbr_line)
2527 if new_r_line:
2528 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002529 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002530 if new_r_line:
2531 self.append_footer(new_r_line)
2532 if new_tbr_line:
2533 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002534
2535 def prompt(self):
2536 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002537 self.set_description([
2538 '# Enter a description of the change.',
2539 '# This will be displayed on the codereview site.',
2540 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002541 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002542 '--------------------',
2543 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002544
agable@chromium.org42c20792013-09-12 17:34:49 +00002545 regexp = re.compile(self.BUG_LINE)
2546 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00002547 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00002548 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002549 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002550 if not content:
2551 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002552 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002553
2554 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002555 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2556 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002557 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002558 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002559
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002560 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00002561 if self._description_lines:
2562 # Add an empty line if either the last line or the new line isn't a tag.
2563 last_line = self._description_lines[-1]
2564 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
2565 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2566 self._description_lines.append('')
2567 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002568
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002569 def get_reviewers(self):
2570 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002571 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
2572 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002573 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002574
2575
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002576def get_approving_reviewers(props):
2577 """Retrieves the reviewers that approved a CL from the issue properties with
2578 messages.
2579
2580 Note that the list may contain reviewers that are not committer, thus are not
2581 considered by the CQ.
2582 """
2583 return sorted(
2584 set(
2585 message['sender']
2586 for message in props['messages']
2587 if message['approval'] and message['sender'] in props['reviewers']
2588 )
2589 )
2590
2591
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002592def FindCodereviewSettingsFile(filename='codereview.settings'):
2593 """Finds the given file starting in the cwd and going up.
2594
2595 Only looks up to the top of the repository unless an
2596 'inherit-review-settings-ok' file exists in the root of the repository.
2597 """
2598 inherit_ok_file = 'inherit-review-settings-ok'
2599 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002600 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002601 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2602 root = '/'
2603 while True:
2604 if filename in os.listdir(cwd):
2605 if os.path.isfile(os.path.join(cwd, filename)):
2606 return open(os.path.join(cwd, filename))
2607 if cwd == root:
2608 break
2609 cwd = os.path.dirname(cwd)
2610
2611
2612def LoadCodereviewSettingsFromFile(fileobj):
2613 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002614 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002615
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002616 def SetProperty(name, setting, unset_error_ok=False):
2617 fullname = 'rietveld.' + name
2618 if setting in keyvals:
2619 RunGit(['config', fullname, keyvals[setting]])
2620 else:
2621 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2622
2623 SetProperty('server', 'CODE_REVIEW_SERVER')
2624 # Only server setting is required. Other settings can be absent.
2625 # In that case, we ignore errors raised during option deletion attempt.
2626 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002627 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002628 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2629 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002630 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002631 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002632 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2633 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002634 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002635 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002636 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002637 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2638 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002639
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002640 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002641 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002642
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002643 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
2644 RunGit(['config', 'gerrit.squash-uploads',
2645 keyvals['GERRIT_SQUASH_UPLOADS']])
2646
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002647 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2648 #should be of the form
2649 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2650 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2651 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2652 keyvals['ORIGIN_URL_CONFIG']])
2653
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002654
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002655def urlretrieve(source, destination):
2656 """urllib is broken for SSL connections via a proxy therefore we
2657 can't use urllib.urlretrieve()."""
2658 with open(destination, 'w') as f:
2659 f.write(urllib2.urlopen(source).read())
2660
2661
ukai@chromium.org712d6102013-11-27 00:52:58 +00002662def hasSheBang(fname):
2663 """Checks fname is a #! script."""
2664 with open(fname) as f:
2665 return f.read(2).startswith('#!')
2666
2667
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00002668# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
2669def DownloadHooks(*args, **kwargs):
2670 pass
2671
2672
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002673def DownloadGerritHook(force):
2674 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002675
2676 Args:
2677 force: True to update hooks. False to install hooks if not present.
2678 """
2679 if not settings.GetIsGerrit():
2680 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00002681 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002682 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2683 if not os.access(dst, os.X_OK):
2684 if os.path.exists(dst):
2685 if not force:
2686 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002687 try:
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002688 print(
2689 'WARNING: installing Gerrit commit-msg hook.\n'
2690 ' This behavior of git cl will soon be disabled.\n'
2691 ' See bug http://crbug.com/579176.')
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002692 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002693 if not hasSheBang(dst):
2694 DieWithError('Not a script: %s\n'
2695 'You need to download from\n%s\n'
2696 'into .git/hooks/commit-msg and '
2697 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002698 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2699 except Exception:
2700 if os.path.exists(dst):
2701 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002702 DieWithError('\nFailed to download hooks.\n'
2703 'You need to download from\n%s\n'
2704 'into .git/hooks/commit-msg and '
2705 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002706
2707
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002708
2709def GetRietveldCodereviewSettingsInteractively():
2710 """Prompt the user for settings."""
2711 server = settings.GetDefaultServerUrl(error_ok=True)
2712 prompt = 'Rietveld server (host[:port])'
2713 prompt += ' [%s]' % (server or DEFAULT_SERVER)
2714 newserver = ask_for_data(prompt + ':')
2715 if not server and not newserver:
2716 newserver = DEFAULT_SERVER
2717 if newserver:
2718 newserver = gclient_utils.UpgradeToHttps(newserver)
2719 if newserver != server:
2720 RunGit(['config', 'rietveld.server', newserver])
2721
2722 def SetProperty(initial, caption, name, is_url):
2723 prompt = caption
2724 if initial:
2725 prompt += ' ("x" to clear) [%s]' % initial
2726 new_val = ask_for_data(prompt + ':')
2727 if new_val == 'x':
2728 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
2729 elif new_val:
2730 if is_url:
2731 new_val = gclient_utils.UpgradeToHttps(new_val)
2732 if new_val != initial:
2733 RunGit(['config', 'rietveld.' + name, new_val])
2734
2735 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
2736 SetProperty(settings.GetDefaultPrivateFlag(),
2737 'Private flag (rietveld only)', 'private', False)
2738 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
2739 'tree-status-url', False)
2740 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
2741 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
2742 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
2743 'run-post-upload-hook', False)
2744
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002745@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002746def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002747 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002748
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002749 print('WARNING: git cl config works for Rietveld only.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002750 'For Gerrit, see http://crbug.com/603116.')
2751 # TODO(tandrii): add Gerrit support as part of http://crbug.com/603116.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00002752 parser.add_option('--activate-update', action='store_true',
2753 help='activate auto-updating [rietveld] section in '
2754 '.git/config')
2755 parser.add_option('--deactivate-update', action='store_true',
2756 help='deactivate auto-updating [rietveld] section in '
2757 '.git/config')
2758 options, args = parser.parse_args(args)
2759
2760 if options.deactivate_update:
2761 RunGit(['config', 'rietveld.autoupdate', 'false'])
2762 return
2763
2764 if options.activate_update:
2765 RunGit(['config', '--unset', 'rietveld.autoupdate'])
2766 return
2767
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002768 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002769 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002770 return 0
2771
2772 url = args[0]
2773 if not url.endswith('codereview.settings'):
2774 url = os.path.join(url, 'codereview.settings')
2775
2776 # Load code review settings and download hooks (if available).
2777 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
2778 return 0
2779
2780
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002781def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002782 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002783 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
2784 branch = ShortBranchName(branchref)
2785 _, args = parser.parse_args(args)
2786 if not args:
2787 print("Current base-url:")
2788 return RunGit(['config', 'branch.%s.base-url' % branch],
2789 error_ok=False).strip()
2790 else:
2791 print("Setting base-url to %s" % args[0])
2792 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
2793 error_ok=False).strip()
2794
2795
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002796def color_for_status(status):
2797 """Maps a Changelist status to color, for CMDstatus and other tools."""
2798 return {
2799 'unsent': Fore.RED,
2800 'waiting': Fore.BLUE,
2801 'reply': Fore.YELLOW,
2802 'lgtm': Fore.GREEN,
2803 'commit': Fore.MAGENTA,
2804 'closed': Fore.CYAN,
2805 'error': Fore.WHITE,
2806 }.get(status, Fore.WHITE)
2807
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002808def fetch_cl_status(branch, auth_config=None):
2809 """Fetches information for an issue and returns (branch, issue, status)."""
2810 cl = Changelist(branchref=branch, auth_config=auth_config)
2811 url = cl.GetIssueURL()
2812 status = cl.GetStatus()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002813
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002814 if url and (not status or status == 'error'):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002815 # The issue probably doesn't exist anymore.
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002816 url += ' (broken)'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002817
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002818 return (branch, url, status)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002819
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002820def get_cl_statuses(
2821 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002822 """Returns a blocking iterable of (branch, issue, color) for given branches.
2823
2824 If fine_grained is true, this will fetch CL statuses from the server.
2825 Otherwise, simply indicate if there's a matching url for the given branches.
2826
2827 If max_processes is specified, it is used as the maximum number of processes
2828 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
2829 spawned.
2830 """
2831 # Silence upload.py otherwise it becomes unwieldly.
2832 upload.verbosity = 0
2833
2834 if fine_grained:
2835 # Process one branch synchronously to work through authentication, then
2836 # spawn processes to process all the other branches in parallel.
2837 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002838 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
2839 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002840
2841 branches_to_fetch = branches[1:]
2842 pool = ThreadPool(
2843 min(max_processes, len(branches_to_fetch))
2844 if max_processes is not None
2845 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002846 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002847 yield x
2848 else:
2849 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
2850 for b in branches:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002851 cl = Changelist(branchref=b, auth_config=auth_config)
2852 url = cl.GetIssueURL()
2853 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002854
rmistry@google.com2dd99862015-06-22 12:22:18 +00002855
2856def upload_branch_deps(cl, args):
2857 """Uploads CLs of local branches that are dependents of the current branch.
2858
2859 If the local branch dependency tree looks like:
2860 test1 -> test2.1 -> test3.1
2861 -> test3.2
2862 -> test2.2 -> test3.3
2863
2864 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
2865 run on the dependent branches in this order:
2866 test2.1, test3.1, test3.2, test2.2, test3.3
2867
2868 Note: This function does not rebase your local dependent branches. Use it when
2869 you make a change to the parent branch that will not conflict with its
2870 dependent branches, and you would like their dependencies updated in
2871 Rietveld.
2872 """
2873 if git_common.is_dirty_git_tree('upload-branch-deps'):
2874 return 1
2875
2876 root_branch = cl.GetBranch()
2877 if root_branch is None:
2878 DieWithError('Can\'t find dependent branches from detached HEAD state. '
2879 'Get on a branch!')
2880 if not cl.GetIssue() or not cl.GetPatchset():
2881 DieWithError('Current branch does not have an uploaded CL. We cannot set '
2882 'patchset dependencies without an uploaded CL.')
2883
2884 branches = RunGit(['for-each-ref',
2885 '--format=%(refname:short) %(upstream:short)',
2886 'refs/heads'])
2887 if not branches:
2888 print('No local branches found.')
2889 return 0
2890
2891 # Create a dictionary of all local branches to the branches that are dependent
2892 # on it.
2893 tracked_to_dependents = collections.defaultdict(list)
2894 for b in branches.splitlines():
2895 tokens = b.split()
2896 if len(tokens) == 2:
2897 branch_name, tracked = tokens
2898 tracked_to_dependents[tracked].append(branch_name)
2899
2900 print
2901 print 'The dependent local branches of %s are:' % root_branch
2902 dependents = []
2903 def traverse_dependents_preorder(branch, padding=''):
2904 dependents_to_process = tracked_to_dependents.get(branch, [])
2905 padding += ' '
2906 for dependent in dependents_to_process:
2907 print '%s%s' % (padding, dependent)
2908 dependents.append(dependent)
2909 traverse_dependents_preorder(dependent, padding)
2910 traverse_dependents_preorder(root_branch)
2911 print
2912
2913 if not dependents:
2914 print 'There are no dependent local branches for %s' % root_branch
2915 return 0
2916
2917 print ('This command will checkout all dependent branches and run '
2918 '"git cl upload".')
2919 ask_for_data('[Press enter to continue or ctrl-C to quit]')
2920
andybons@chromium.org962f9462016-02-03 20:00:42 +00002921 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00002922 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00002923 args.extend(['-t', 'Updated patchset dependency'])
2924
rmistry@google.com2dd99862015-06-22 12:22:18 +00002925 # Record all dependents that failed to upload.
2926 failures = {}
2927 # Go through all dependents, checkout the branch and upload.
2928 try:
2929 for dependent_branch in dependents:
2930 print
2931 print '--------------------------------------'
2932 print 'Running "git cl upload" from %s:' % dependent_branch
2933 RunGit(['checkout', '-q', dependent_branch])
2934 print
2935 try:
2936 if CMDupload(OptionParser(), args) != 0:
2937 print 'Upload failed for %s!' % dependent_branch
2938 failures[dependent_branch] = 1
2939 except: # pylint: disable=W0702
2940 failures[dependent_branch] = 1
2941 print
2942 finally:
2943 # Swap back to the original root branch.
2944 RunGit(['checkout', '-q', root_branch])
2945
2946 print
2947 print 'Upload complete for dependent branches!'
2948 for dependent_branch in dependents:
2949 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
2950 print ' %s : %s' % (dependent_branch, upload_status)
2951 print
2952
2953 return 0
2954
2955
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002956def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002957 """Show status of changelists.
2958
2959 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00002960 - Red not sent for review or broken
2961 - Blue waiting for review
2962 - Yellow waiting for you to reply to review
2963 - Green LGTM'ed
2964 - Magenta in the commit queue
2965 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002966
2967 Also see 'git cl comments'.
2968 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002969 parser.add_option('--field',
2970 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002971 parser.add_option('-f', '--fast', action='store_true',
2972 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002973 parser.add_option(
2974 '-j', '--maxjobs', action='store', type=int,
2975 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002976
2977 auth.add_auth_options(parser)
2978 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002979 if args:
2980 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002981 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002982
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002983 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002984 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002985 if options.field.startswith('desc'):
2986 print cl.GetDescription()
2987 elif options.field == 'id':
2988 issueid = cl.GetIssue()
2989 if issueid:
2990 print issueid
2991 elif options.field == 'patch':
2992 patchset = cl.GetPatchset()
2993 if patchset:
2994 print patchset
2995 elif options.field == 'url':
2996 url = cl.GetIssueURL()
2997 if url:
2998 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002999 return 0
3000
3001 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3002 if not branches:
3003 print('No local branch found.')
3004 return 0
3005
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003006 changes = (
3007 Changelist(branchref=b, auth_config=auth_config)
3008 for b in branches.splitlines())
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003009 # TODO(tandrii): refactor to use CLs list instead of branches list.
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00003010 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003011 alignment = max(5, max(len(b) for b in branches))
3012 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003013 output = get_cl_statuses(branches,
3014 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003015 max_processes=options.maxjobs,
3016 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003017
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003018 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003019 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003020 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003021 while branch not in branch_statuses:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003022 b, i, status = output.next()
3023 branch_statuses[b] = (i, status)
3024 issue_url, status = branch_statuses.pop(branch)
3025 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003026 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003027 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003028 color = ''
3029 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003030 status_str = '(%s)' % status if status else ''
3031 print ' %*s : %s%s %s%s' % (
3032 alignment, ShortBranchName(branch), color, issue_url, status_str,
3033 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003034
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003035 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003036 print
3037 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003038 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003039 if not cl.GetIssue():
3040 print 'No issue assigned.'
3041 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003042 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00003043 if not options.fast:
3044 print 'Issue description:'
3045 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003046 return 0
3047
3048
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003049def colorize_CMDstatus_doc():
3050 """To be called once in main() to add colors to git cl status help."""
3051 colors = [i for i in dir(Fore) if i[0].isupper()]
3052
3053 def colorize_line(line):
3054 for color in colors:
3055 if color in line.upper():
3056 # Extract whitespaces first and the leading '-'.
3057 indent = len(line) - len(line.lstrip(' ')) + 1
3058 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3059 return line
3060
3061 lines = CMDstatus.__doc__.splitlines()
3062 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3063
3064
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003065@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003066def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003067 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003068
3069 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003070 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003071 parser.add_option('-r', '--reverse', action='store_true',
3072 help='Lookup the branch(es) for the specified issues. If '
3073 'no issues are specified, all branches with mapped '
3074 'issues will be listed.')
3075 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003076
dnj@chromium.org406c4402015-03-03 17:22:28 +00003077 if options.reverse:
3078 branches = RunGit(['for-each-ref', 'refs/heads',
3079 '--format=%(refname:short)']).splitlines()
3080
3081 # Reverse issue lookup.
3082 issue_branch_map = {}
3083 for branch in branches:
3084 cl = Changelist(branchref=branch)
3085 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3086 if not args:
3087 args = sorted(issue_branch_map.iterkeys())
3088 for issue in args:
3089 if not issue:
3090 continue
3091 print 'Branch for issue number %s: %s' % (
3092 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
3093 else:
3094 cl = Changelist()
3095 if len(args) > 0:
3096 try:
3097 issue = int(args[0])
3098 except ValueError:
3099 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003100 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003101 cl.SetIssue(issue)
3102 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003103 return 0
3104
3105
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003106def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003107 """Shows or posts review comments for any changelist."""
3108 parser.add_option('-a', '--add-comment', dest='comment',
3109 help='comment to add to an issue')
3110 parser.add_option('-i', dest='issue',
3111 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003112 parser.add_option('-j', '--json-file',
3113 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003114 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003115 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003116 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003117
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003118 issue = None
3119 if options.issue:
3120 try:
3121 issue = int(options.issue)
3122 except ValueError:
3123 DieWithError('A review issue id is expected to be a number')
3124
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003125 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003126
3127 if options.comment:
3128 cl.AddComment(options.comment)
3129 return 0
3130
3131 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003132 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003133 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003134 summary.append({
3135 'date': message['date'],
3136 'lgtm': False,
3137 'message': message['text'],
3138 'not_lgtm': False,
3139 'sender': message['sender'],
3140 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003141 if message['disapproval']:
3142 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003143 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003144 elif message['approval']:
3145 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003146 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003147 elif message['sender'] == data['owner_email']:
3148 color = Fore.MAGENTA
3149 else:
3150 color = Fore.BLUE
3151 print '\n%s%s %s%s' % (
3152 color, message['date'].split('.', 1)[0], message['sender'],
3153 Fore.RESET)
3154 if message['text'].strip():
3155 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00003156 if options.json_file:
3157 with open(options.json_file, 'wb') as f:
3158 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003159 return 0
3160
3161
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003162def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003163 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003164 parser.add_option('-d', '--display', action='store_true',
3165 help='Display the description instead of opening an editor')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003166 auth.add_auth_options(parser)
3167 options, _ = parser.parse_args(args)
3168 auth_config = auth.extract_auth_config_from_options(options)
3169 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003170 if not cl.GetIssue():
3171 DieWithError('This branch has no associated changelist.')
3172 description = ChangeDescription(cl.GetDescription())
smut@google.com34fb6b12015-07-13 20:03:26 +00003173 if options.display:
3174 print description.description
3175 return 0
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003176 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003177 if cl.GetDescription() != description.description:
3178 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003179 return 0
3180
3181
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003182def CreateDescriptionFromLog(args):
3183 """Pulls out the commit log to use as a base for the CL description."""
3184 log_args = []
3185 if len(args) == 1 and not args[0].endswith('.'):
3186 log_args = [args[0] + '..']
3187 elif len(args) == 1 and args[0].endswith('...'):
3188 log_args = [args[0][:-1]]
3189 elif len(args) == 2:
3190 log_args = [args[0] + '..' + args[1]]
3191 else:
3192 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003193 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003194
3195
thestig@chromium.org44202a22014-03-11 19:22:18 +00003196def CMDlint(parser, args):
3197 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003198 parser.add_option('--filter', action='append', metavar='-x,+y',
3199 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003200 auth.add_auth_options(parser)
3201 options, args = parser.parse_args(args)
3202 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003203
3204 # Access to a protected member _XX of a client class
3205 # pylint: disable=W0212
3206 try:
3207 import cpplint
3208 import cpplint_chromium
3209 except ImportError:
3210 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
3211 return 1
3212
3213 # Change the current working directory before calling lint so that it
3214 # shows the correct base.
3215 previous_cwd = os.getcwd()
3216 os.chdir(settings.GetRoot())
3217 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003218 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003219 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3220 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003221 if not files:
3222 print "Cannot lint an empty CL"
3223 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003224
3225 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003226 command = args + files
3227 if options.filter:
3228 command = ['--filter=' + ','.join(options.filter)] + command
3229 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003230
3231 white_regex = re.compile(settings.GetLintRegex())
3232 black_regex = re.compile(settings.GetLintIgnoreRegex())
3233 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3234 for filename in filenames:
3235 if white_regex.match(filename):
3236 if black_regex.match(filename):
3237 print "Ignoring file %s" % filename
3238 else:
3239 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3240 extra_check_functions)
3241 else:
3242 print "Skipping file %s" % filename
3243 finally:
3244 os.chdir(previous_cwd)
3245 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
3246 if cpplint._cpplint_state.error_count != 0:
3247 return 1
3248 return 0
3249
3250
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003251def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003252 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003253 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003254 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003255 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003256 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003257 auth.add_auth_options(parser)
3258 options, args = parser.parse_args(args)
3259 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003260
sbc@chromium.org71437c02015-04-09 19:29:40 +00003261 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00003262 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003263 return 1
3264
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003265 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003266 if args:
3267 base_branch = args[0]
3268 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003269 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003270 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003271
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003272 cl.RunHook(
3273 committing=not options.upload,
3274 may_prompt=False,
3275 verbose=options.verbose,
3276 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003277 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003278
3279
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003280def GenerateGerritChangeId(message):
3281 """Returns Ixxxxxx...xxx change id.
3282
3283 Works the same way as
3284 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3285 but can be called on demand on all platforms.
3286
3287 The basic idea is to generate git hash of a state of the tree, original commit
3288 message, author/committer info and timestamps.
3289 """
3290 lines = []
3291 tree_hash = RunGitSilent(['write-tree'])
3292 lines.append('tree %s' % tree_hash.strip())
3293 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3294 if code == 0:
3295 lines.append('parent %s' % parent.strip())
3296 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3297 lines.append('author %s' % author.strip())
3298 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3299 lines.append('committer %s' % committer.strip())
3300 lines.append('')
3301 # Note: Gerrit's commit-hook actually cleans message of some lines and
3302 # whitespace. This code is not doing this, but it clearly won't decrease
3303 # entropy.
3304 lines.append(message)
3305 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3306 stdin='\n'.join(lines))
3307 return 'I%s' % change_hash.strip()
3308
3309
wittman@chromium.org455dc922015-01-26 20:15:50 +00003310def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3311 """Computes the remote branch ref to use for the CL.
3312
3313 Args:
3314 remote (str): The git remote for the CL.
3315 remote_branch (str): The git remote branch for the CL.
3316 target_branch (str): The target branch specified by the user.
3317 pending_prefix (str): The pending prefix from the settings.
3318 """
3319 if not (remote and remote_branch):
3320 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003321
wittman@chromium.org455dc922015-01-26 20:15:50 +00003322 if target_branch:
3323 # Cannonicalize branch references to the equivalent local full symbolic
3324 # refs, which are then translated into the remote full symbolic refs
3325 # below.
3326 if '/' not in target_branch:
3327 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3328 else:
3329 prefix_replacements = (
3330 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3331 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3332 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3333 )
3334 match = None
3335 for regex, replacement in prefix_replacements:
3336 match = re.search(regex, target_branch)
3337 if match:
3338 remote_branch = target_branch.replace(match.group(0), replacement)
3339 break
3340 if not match:
3341 # This is a branch path but not one we recognize; use as-is.
3342 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003343 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3344 # Handle the refs that need to land in different refs.
3345 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003346
wittman@chromium.org455dc922015-01-26 20:15:50 +00003347 # Create the true path to the remote branch.
3348 # Does the following translation:
3349 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3350 # * refs/remotes/origin/master -> refs/heads/master
3351 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3352 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3353 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3354 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3355 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3356 'refs/heads/')
3357 elif remote_branch.startswith('refs/remotes/branch-heads'):
3358 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3359 # If a pending prefix exists then replace refs/ with it.
3360 if pending_prefix:
3361 remote_branch = remote_branch.replace('refs/', pending_prefix)
3362 return remote_branch
3363
3364
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003365def cleanup_list(l):
3366 """Fixes a list so that comma separated items are put as individual items.
3367
3368 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3369 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3370 """
3371 items = sum((i.split(',') for i in l), [])
3372 stripped_items = (i.strip() for i in items)
3373 return sorted(filter(None, stripped_items))
3374
3375
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003376@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003377def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003378 """Uploads the current changelist to codereview.
3379
3380 Can skip dependency patchset uploads for a branch by running:
3381 git config branch.branch_name.skip-deps-uploads True
3382 To unset run:
3383 git config --unset branch.branch_name.skip-deps-uploads
3384 Can also set the above globally by using the --global flag.
3385 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003386 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3387 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003388 parser.add_option('--bypass-watchlists', action='store_true',
3389 dest='bypass_watchlists',
3390 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003391 parser.add_option('-f', action='store_true', dest='force',
3392 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003393 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003394 parser.add_option('-t', dest='title',
3395 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003396 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003397 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003398 help='reviewer email addresses')
3399 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003400 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003401 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003402 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003403 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003404 parser.add_option('--emulate_svn_auto_props',
3405 '--emulate-svn-auto-props',
3406 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003407 dest="emulate_svn_auto_props",
3408 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003409 parser.add_option('-c', '--use-commit-queue', action='store_true',
3410 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003411 parser.add_option('--private', action='store_true',
3412 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003413 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003414 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003415 metavar='TARGET',
3416 help='Apply CL to remote ref TARGET. ' +
3417 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003418 parser.add_option('--squash', action='store_true',
3419 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003420 parser.add_option('--no-squash', action='store_true',
3421 help='Don\'t squash multiple commits into one ' +
3422 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003423 parser.add_option('--email', default=None,
3424 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003425 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3426 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003427 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3428 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003429 help='Send the patchset to do a CQ dry run right after '
3430 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003431 parser.add_option('--dependencies', action='store_true',
3432 help='Uploads CLs of all the local branches that depend on '
3433 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003434
rmistry@google.com2dd99862015-06-22 12:22:18 +00003435 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003436 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003437 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003438 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003439 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003440
sbc@chromium.org71437c02015-04-09 19:29:40 +00003441 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003442 return 1
3443
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003444 options.reviewers = cleanup_list(options.reviewers)
3445 options.cc = cleanup_list(options.cc)
3446
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003447 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3448 settings.GetIsGerrit()
3449
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003450 cl = Changelist(auth_config=auth_config)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00003451 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003452
3453
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003454def IsSubmoduleMergeCommit(ref):
3455 # When submodules are added to the repo, we expect there to be a single
3456 # non-git-svn merge commit at remote HEAD with a signature comment.
3457 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003458 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003459 return RunGit(cmd) != ''
3460
3461
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003462def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003463 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003464
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003465 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3466 upstream and closes the issue automatically and atomically.
3467
3468 Otherwise (in case of Rietveld):
3469 Squashes branch into a single commit.
3470 Updates changelog with metadata (e.g. pointer to review).
3471 Pushes/dcommits the code upstream.
3472 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003473 """
3474 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3475 help='bypass upload presubmit hook')
3476 parser.add_option('-m', dest='message',
3477 help="override review description")
3478 parser.add_option('-f', action='store_true', dest='force',
3479 help="force yes to questions (don't prompt)")
3480 parser.add_option('-c', dest='contributor',
3481 help="external contributor for patch (appended to " +
3482 "description and used as author for git). Should be " +
3483 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003484 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003485 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003486 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003487 auth_config = auth.extract_auth_config_from_options(options)
3488
3489 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003490
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003491 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3492 if cl.IsGerrit():
3493 if options.message:
3494 # This could be implemented, but it requires sending a new patch to
3495 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3496 # Besides, Gerrit has the ability to change the commit message on submit
3497 # automatically, thus there is no need to support this option (so far?).
3498 parser.error('-m MESSAGE option is not supported for Gerrit.')
3499 if options.contributor:
3500 parser.error(
3501 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3502 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3503 'the contributor\'s "name <email>". If you can\'t upload such a '
3504 'commit for review, contact your repository admin and request'
3505 '"Forge-Author" permission.')
3506 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3507 options.verbose)
3508
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003509 current = cl.GetBranch()
3510 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3511 if not settings.GetIsGitSvn() and remote == '.':
3512 print
3513 print 'Attempting to push branch %r into another local branch!' % current
3514 print
3515 print 'Either reparent this branch on top of origin/master:'
3516 print ' git reparent-branch --root'
3517 print
3518 print 'OR run `git rebase-update` if you think the parent branch is already'
3519 print 'committed.'
3520 print
3521 print ' Current parent: %r' % upstream_branch
3522 return 1
3523
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003524 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003525 # Default to merging against our best guess of the upstream branch.
3526 args = [cl.GetUpstreamBranch()]
3527
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003528 if options.contributor:
3529 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
3530 print "Please provide contibutor as 'First Last <email@example.com>'"
3531 return 1
3532
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003533 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003534 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003535
sbc@chromium.org71437c02015-04-09 19:29:40 +00003536 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003537 return 1
3538
3539 # This rev-list syntax means "show all commits not in my branch that
3540 # are in base_branch".
3541 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3542 base_branch]).splitlines()
3543 if upstream_commits:
3544 print ('Base branch "%s" has %d commits '
3545 'not in this branch.' % (base_branch, len(upstream_commits)))
3546 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
3547 return 1
3548
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003549 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003550 svn_head = None
3551 if cmd == 'dcommit' or base_has_submodules:
3552 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
3553 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003554
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003555 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003556 # If the base_head is a submodule merge commit, the first parent of the
3557 # base_head should be a git-svn commit, which is what we're interested in.
3558 base_svn_head = base_branch
3559 if base_has_submodules:
3560 base_svn_head += '^1'
3561
3562 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003563 if extra_commits:
3564 print ('This branch has %d additional commits not upstreamed yet.'
3565 % len(extra_commits.splitlines()))
3566 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
3567 'before attempting to %s.' % (base_branch, cmd))
3568 return 1
3569
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003570 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003571 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003572 author = None
3573 if options.contributor:
3574 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003575 hook_results = cl.RunHook(
3576 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003577 may_prompt=not options.force,
3578 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003579 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003580 if not hook_results.should_continue():
3581 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003582
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003583 # Check the tree status if the tree status URL is set.
3584 status = GetTreeStatus()
3585 if 'closed' == status:
3586 print('The tree is closed. Please wait for it to reopen. Use '
3587 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3588 return 1
3589 elif 'unknown' == status:
3590 print('Unable to determine tree status. Please verify manually and '
3591 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3592 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003593
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003594 change_desc = ChangeDescription(options.message)
3595 if not change_desc.description and cl.GetIssue():
3596 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003597
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003598 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00003599 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003600 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00003601 else:
3602 print 'No description set.'
3603 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
3604 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003605
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003606 # Keep a separate copy for the commit message, because the commit message
3607 # contains the link to the Rietveld issue, while the Rietveld message contains
3608 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003609 # Keep a separate copy for the commit message.
3610 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00003611 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003612
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003613 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00003614 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00003615 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00003616 # after it. Add a period on a new line to circumvent this. Also add a space
3617 # before the period to make sure that Gitiles continues to correctly resolve
3618 # the URL.
3619 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003620 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003621 commit_desc.append_footer('Patch from %s.' % options.contributor)
3622
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00003623 print('Description:')
3624 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003625
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003626 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003627 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00003628 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003629
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003630 # We want to squash all this branch's commits into one commit with the proper
3631 # description. We do this by doing a "reset --soft" to the base branch (which
3632 # keeps the working copy the same), then dcommitting that. If origin/master
3633 # has a submodule merge commit, we'll also need to cherry-pick the squashed
3634 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003635 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003636 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
3637 # Delete the branches if they exist.
3638 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
3639 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
3640 result = RunGitWithCode(showref_cmd)
3641 if result[0] == 0:
3642 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003643
3644 # We might be in a directory that's present in this branch but not in the
3645 # trunk. Move up to the top of the tree so that git commands that expect a
3646 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003647 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003648 if rel_base_path:
3649 os.chdir(rel_base_path)
3650
3651 # Stuff our change into the merge branch.
3652 # We wrap in a try...finally block so if anything goes wrong,
3653 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003654 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003655 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003656 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003657 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003658 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00003659 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003660 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003661 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003662 RunGit(
3663 [
3664 'commit', '--author', options.contributor,
3665 '-m', commit_desc.description,
3666 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003667 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003668 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003669 if base_has_submodules:
3670 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
3671 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
3672 RunGit(['checkout', CHERRY_PICK_BRANCH])
3673 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003674 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003675 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003676 mirror = settings.GetGitMirror(remote)
3677 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003678 pending_prefix = settings.GetPendingRefPrefix()
3679 if not pending_prefix or branch.startswith(pending_prefix):
3680 # If not using refs/pending/heads/* at all, or target ref is already set
3681 # to pending, then push to the target ref directly.
3682 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003683 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003684 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003685 else:
3686 # Cherry-pick the change on top of pending ref and then push it.
3687 assert branch.startswith('refs/'), branch
3688 assert pending_prefix[-1] == '/', pending_prefix
3689 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003690 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003691 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003692 if retcode == 0:
3693 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003694 else:
3695 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003696 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003697 'svn', 'dcommit',
3698 '-C%s' % options.similarity,
3699 '--no-rebase', '--rmdir',
3700 ]
3701 if settings.GetForceHttpsCommitUrl():
3702 # Allow forcing https commit URLs for some projects that don't allow
3703 # committing to http URLs (like Google Code).
3704 remote_url = cl.GetGitSvnRemoteUrl()
3705 if urlparse.urlparse(remote_url).scheme == 'http':
3706 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003707 cmd_args.append('--commit-url=%s' % remote_url)
3708 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003709 if 'Committed r' in output:
3710 revision = re.match(
3711 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
3712 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003713 finally:
3714 # And then swap back to the original branch and clean up.
3715 RunGit(['checkout', '-q', cl.GetBranch()])
3716 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003717 if base_has_submodules:
3718 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003719
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003720 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003721 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003722 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003723
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003724 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003725 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003726 try:
3727 revision = WaitForRealCommit(remote, revision, base_branch, branch)
3728 # We set pushed_to_pending to False, since it made it all the way to the
3729 # real ref.
3730 pushed_to_pending = False
3731 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003732 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003733
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003734 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003735 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003736 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003737 if not to_pending:
3738 if viewvc_url and revision:
3739 change_desc.append_footer(
3740 'Committed: %s%s' % (viewvc_url, revision))
3741 elif revision:
3742 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003743 print ('Closing issue '
3744 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003745 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003746 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003747 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00003748 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00003749 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00003750 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003751 if options.bypass_hooks:
3752 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
3753 else:
3754 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00003755 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003756 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003757
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003758 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003759 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
3760 print 'The commit is in the pending queue (%s).' % pending_ref
3761 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00003762 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003763 'footer.' % branch)
3764
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003765 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
3766 if os.path.isfile(hook):
3767 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003768
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003769 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003770
3771
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003772def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
3773 print
3774 print 'Waiting for commit to be landed on %s...' % real_ref
3775 print '(If you are impatient, you may Ctrl-C once without harm)'
3776 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
3777 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003778 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003779
3780 loop = 0
3781 while True:
3782 sys.stdout.write('fetching (%d)... \r' % loop)
3783 sys.stdout.flush()
3784 loop += 1
3785
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003786 if mirror:
3787 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003788 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
3789 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
3790 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
3791 for commit in commits.splitlines():
3792 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
3793 print 'Found commit on %s' % real_ref
3794 return commit
3795
3796 current_rev = to_rev
3797
3798
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003799def PushToGitPending(remote, pending_ref, upstream_ref):
3800 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
3801
3802 Returns:
3803 (retcode of last operation, output log of last operation).
3804 """
3805 assert pending_ref.startswith('refs/'), pending_ref
3806 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
3807 cherry = RunGit(['rev-parse', 'HEAD']).strip()
3808 code = 0
3809 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003810 max_attempts = 3
3811 attempts_left = max_attempts
3812 while attempts_left:
3813 if attempts_left != max_attempts:
3814 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
3815 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003816
3817 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003818 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003819 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003820 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003821 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003822 print 'Fetch failed with exit code %d.' % code
3823 if out.strip():
3824 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003825 continue
3826
3827 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003828 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003829 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003830 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003831 if code:
3832 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003833 'Your patch doesn\'t apply cleanly to ref \'%s\', '
3834 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003835 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
3836 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003837 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003838 return code, out
3839
3840 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003841 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003842 code, out = RunGitWithCode(
3843 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
3844 if code == 0:
3845 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003846 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003847 return code, out
3848
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003849 print 'Push failed with exit code %d.' % code
3850 if out.strip():
3851 print out.strip()
3852 if IsFatalPushFailure(out):
3853 print (
3854 'Fatal push error. Make sure your .netrc credentials and git '
3855 'user.email are correct and you have push access to the repo.')
3856 return code, out
3857
3858 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003859 return code, out
3860
3861
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003862def IsFatalPushFailure(push_stdout):
3863 """True if retrying push won't help."""
3864 return '(prohibited by Gerrit)' in push_stdout
3865
3866
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003867@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003868def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003869 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003870 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003871 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003872 # If it looks like previous commits were mirrored with git-svn.
3873 message = """This repository appears to be a git-svn mirror, but no
3874upstream SVN master is set. You probably need to run 'git auto-svn' once."""
3875 else:
3876 message = """This doesn't appear to be an SVN repository.
3877If your project has a true, writeable git repository, you probably want to run
3878'git cl land' instead.
3879If your project has a git mirror of an upstream SVN master, you probably need
3880to run 'git svn init'.
3881
3882Using the wrong command might cause your commit to appear to succeed, and the
3883review to be closed, without actually landing upstream. If you choose to
3884proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00003885 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00003886 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003887 return SendUpstream(parser, args, 'dcommit')
3888
3889
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003890@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003891def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003892 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003893 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003894 print('This appears to be an SVN repository.')
3895 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003896 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00003897 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003898 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003899
3900
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003901@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003902def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00003903 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003904 parser.add_option('-b', dest='newbranch',
3905 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003906 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003907 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003908 parser.add_option('-d', '--directory', action='store', metavar='DIR',
3909 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003910 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003911 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003912 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003913 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003914 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003915 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003916
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003917
3918 group = optparse.OptionGroup(
3919 parser,
3920 'Options for continuing work on the current issue uploaded from a '
3921 'different clone (e.g. different machine). Must be used independently '
3922 'from the other options. No issue number should be specified, and the '
3923 'branch must have an issue number associated with it')
3924 group.add_option('--reapply', action='store_true', dest='reapply',
3925 help='Reset the branch and reapply the issue.\n'
3926 'CAUTION: This will undo any local changes in this '
3927 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003928
3929 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003930 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003931 parser.add_option_group(group)
3932
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003933 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003934 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003935 auth_config = auth.extract_auth_config_from_options(options)
3936
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003937 cl = Changelist(auth_config=auth_config)
3938
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003939 issue_arg = None
3940 if options.reapply :
3941 if len(args) > 0:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003942 parser.error('--reapply implies no additional arguments.')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003943
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003944 issue_arg = cl.GetIssue()
3945 upstream = cl.GetUpstreamBranch()
3946 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003947 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003948
3949 RunGit(['reset', '--hard', upstream])
3950 if options.pull:
3951 RunGit(['pull'])
3952 else:
3953 if len(args) != 1:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003954 parser.error('Must specify issue number or url')
3955 issue_arg = args[0]
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003956
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003957 if not issue_arg:
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003958 parser.print_help()
3959 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003960
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003961 if cl.IsGerrit():
3962 if options.reject:
3963 parser.error('--reject is not supported with Gerrit codereview.')
3964 if options.nocommit:
3965 parser.error('--nocommit is not supported with Gerrit codereview.')
3966 if options.directory:
3967 parser.error('--directory is not supported with Gerrit codereview.')
3968
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003969 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003970 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003971 return 1
3972
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003973 if options.newbranch:
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003974 if options.reapply:
3975 parser.error("--reapply excludes any option other than --pull")
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003976 if options.force:
3977 RunGit(['branch', '-D', options.newbranch],
3978 stderr=subprocess2.PIPE, error_ok=True)
3979 RunGit(['checkout', '-b', options.newbranch,
3980 Changelist().GetUpstreamBranch()])
3981
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003982 return cl.CMDPatchIssue(issue_arg, options.reject, options.nocommit,
3983 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003984
3985
3986def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003987 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003988 # Provide a wrapper for git svn rebase to help avoid accidental
3989 # git svn dcommit.
3990 # It's the only command that doesn't use parser at all since we just defer
3991 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003992
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003993 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003994
3995
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003996def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003997 """Fetches the tree status and returns either 'open', 'closed',
3998 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003999 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004000 if url:
4001 status = urllib2.urlopen(url).read().lower()
4002 if status.find('closed') != -1 or status == '0':
4003 return 'closed'
4004 elif status.find('open') != -1 or status == '1':
4005 return 'open'
4006 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004007 return 'unset'
4008
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004009
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004010def GetTreeStatusReason():
4011 """Fetches the tree status from a json url and returns the message
4012 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004013 url = settings.GetTreeStatusUrl()
4014 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004015 connection = urllib2.urlopen(json_url)
4016 status = json.loads(connection.read())
4017 connection.close()
4018 return status['message']
4019
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004020
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004021def GetBuilderMaster(bot_list):
4022 """For a given builder, fetch the master from AE if available."""
4023 map_url = 'https://builders-map.appspot.com/'
4024 try:
4025 master_map = json.load(urllib2.urlopen(map_url))
4026 except urllib2.URLError as e:
4027 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4028 (map_url, e))
4029 except ValueError as e:
4030 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4031 if not master_map:
4032 return None, 'Failed to build master map.'
4033
4034 result_master = ''
4035 for bot in bot_list:
4036 builder = bot.split(':', 1)[0]
4037 master_list = master_map.get(builder, [])
4038 if not master_list:
4039 return None, ('No matching master for builder %s.' % builder)
4040 elif len(master_list) > 1:
4041 return None, ('The builder name %s exists in multiple masters %s.' %
4042 (builder, master_list))
4043 else:
4044 cur_master = master_list[0]
4045 if not result_master:
4046 result_master = cur_master
4047 elif result_master != cur_master:
4048 return None, 'The builders do not belong to the same master.'
4049 return result_master, None
4050
4051
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004052def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004053 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004054 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004055 status = GetTreeStatus()
4056 if 'unset' == status:
4057 print 'You must configure your tree status URL by running "git cl config".'
4058 return 2
4059
4060 print "The tree is %s" % status
4061 print
4062 print GetTreeStatusReason()
4063 if status != 'open':
4064 return 1
4065 return 0
4066
4067
maruel@chromium.org15192402012-09-06 12:38:29 +00004068def CMDtry(parser, args):
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004069 """Triggers a try job through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00004070 group = optparse.OptionGroup(parser, "Try job options")
4071 group.add_option(
4072 "-b", "--bot", action="append",
4073 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
4074 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004075 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00004076 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004077 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00004078 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004079 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00004080 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004081 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004082 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004083 "-r", "--revision",
4084 help="Revision to use for the try job; default: the "
4085 "revision will be determined by the try server; see "
4086 "its waterfall for more info")
4087 group.add_option(
4088 "-c", "--clobber", action="store_true", default=False,
4089 help="Force a clobber before building; e.g. don't do an "
4090 "incremental build")
4091 group.add_option(
4092 "--project",
4093 help="Override which project to use. Projects are defined "
4094 "server-side to define what default bot set to use")
4095 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004096 "-p", "--property", dest="properties", action="append", default=[],
4097 help="Specify generic properties in the form -p key1=value1 -p "
4098 "key2=value2 etc (buildbucket only). The value will be treated as "
4099 "json if decodable, or as string otherwise.")
4100 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004101 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004102 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004103 "--use-rietveld", action="store_true", default=False,
4104 help="Use Rietveld to trigger try jobs.")
4105 group.add_option(
4106 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4107 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004108 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004109 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004110 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004111 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004112
machenbach@chromium.org45453142015-09-15 08:45:22 +00004113 if options.use_rietveld and options.properties:
4114 parser.error('Properties can only be specified with buildbucket')
4115
4116 # Make sure that all properties are prop=value pairs.
4117 bad_params = [x for x in options.properties if '=' not in x]
4118 if bad_params:
4119 parser.error('Got properties with missing "=": %s' % bad_params)
4120
maruel@chromium.org15192402012-09-06 12:38:29 +00004121 if args:
4122 parser.error('Unknown arguments: %s' % args)
4123
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004124 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004125 if not cl.GetIssue():
4126 parser.error('Need to upload first')
4127
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004128 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004129 if props.get('closed'):
4130 parser.error('Cannot send tryjobs for a closed CL')
4131
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004132 if props.get('private'):
4133 parser.error('Cannot use trybots with private issue')
4134
maruel@chromium.org15192402012-09-06 12:38:29 +00004135 if not options.name:
4136 options.name = cl.GetBranch()
4137
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004138 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004139 options.master, err_msg = GetBuilderMaster(options.bot)
4140 if err_msg:
4141 parser.error('Tryserver master cannot be found because: %s\n'
4142 'Please manually specify the tryserver master'
4143 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004144
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004145 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004146 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004147 if not options.bot:
4148 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004149
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004150 # Get try masters from PRESUBMIT.py files.
4151 masters = presubmit_support.DoGetTryMasters(
4152 change,
4153 change.LocalPaths(),
4154 settings.GetRoot(),
4155 None,
4156 None,
4157 options.verbose,
4158 sys.stdout)
4159 if masters:
4160 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004161
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004162 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4163 options.bot = presubmit_support.DoGetTrySlaves(
4164 change,
4165 change.LocalPaths(),
4166 settings.GetRoot(),
4167 None,
4168 None,
4169 options.verbose,
4170 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004171
4172 if not options.bot:
4173 # Get try masters from cq.cfg if any.
4174 # TODO(tandrii): some (but very few) projects store cq.cfg in different
4175 # location.
4176 cq_cfg = os.path.join(change.RepositoryRoot(),
4177 'infra', 'config', 'cq.cfg')
4178 if os.path.exists(cq_cfg):
4179 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00004180 cq_masters = commit_queue.get_master_builder_map(
4181 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004182 for master, builders in cq_masters.iteritems():
4183 for builder in builders:
4184 # Skip presubmit builders, because these will fail without LGTM.
4185 if 'presubmit' not in builder.lower():
4186 masters.setdefault(master, {})[builder] = ['defaulttests']
4187 if masters:
4188 return masters
4189
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004190 if not options.bot:
4191 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00004192
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004193 builders_and_tests = {}
4194 # TODO(machenbach): The old style command-line options don't support
4195 # multiple try masters yet.
4196 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4197 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4198
4199 for bot in old_style:
4200 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004201 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004202 elif ',' in bot:
4203 parser.error('Specify one bot per --bot flag')
4204 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004205 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004206
4207 for bot, tests in new_style:
4208 builders_and_tests.setdefault(bot, []).extend(tests)
4209
4210 # Return a master map with one master to be backwards compatible. The
4211 # master name defaults to an empty string, which will cause the master
4212 # not to be set on rietveld (deprecated).
4213 return {options.master: builders_and_tests}
4214
4215 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004216
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004217 for builders in masters.itervalues():
4218 if any('triggered' in b for b in builders):
4219 print >> sys.stderr, (
4220 'ERROR You are trying to send a job to a triggered bot. This type of'
4221 ' bot requires an\ninitial job from a parent (usually a builder). '
4222 'Instead send your job to the parent.\n'
4223 'Bot list: %s' % builders)
4224 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004225
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004226 patchset = cl.GetMostRecentPatchset()
4227 if patchset and patchset != cl.GetPatchset():
4228 print(
4229 '\nWARNING Mismatch between local config and server. Did a previous '
4230 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4231 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004232 if options.luci:
4233 trigger_luci_job(cl, masters, options)
4234 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004235 try:
4236 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4237 except BuildbucketResponseException as ex:
4238 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004239 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004240 except Exception as e:
4241 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4242 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
4243 e, stacktrace)
4244 return 1
4245 else:
4246 try:
4247 cl.RpcServer().trigger_distributed_try_jobs(
4248 cl.GetIssue(), patchset, options.name, options.clobber,
4249 options.revision, masters)
4250 except urllib2.HTTPError as e:
4251 if e.code == 404:
4252 print('404 from rietveld; '
4253 'did you mean to use "git try" instead of "git cl try"?')
4254 return 1
4255 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004256
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004257 for (master, builders) in sorted(masters.iteritems()):
4258 if master:
4259 print 'Master: %s' % master
4260 length = max(len(builder) for builder in builders)
4261 for builder in sorted(builders):
4262 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00004263 return 0
4264
4265
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004266def CMDtry_results(parser, args):
4267 group = optparse.OptionGroup(parser, "Try job results options")
4268 group.add_option(
4269 "-p", "--patchset", type=int, help="patchset number if not current.")
4270 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004271 "--print-master", action='store_true', help="print master name as well.")
4272 group.add_option(
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004273 "--color", action='store_true', default=setup_color.IS_TTY,
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004274 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004275 group.add_option(
4276 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4277 help="Host of buildbucket. The default host is %default.")
4278 parser.add_option_group(group)
4279 auth.add_auth_options(parser)
4280 options, args = parser.parse_args(args)
4281 if args:
4282 parser.error('Unrecognized args: %s' % ' '.join(args))
4283
4284 auth_config = auth.extract_auth_config_from_options(options)
4285 cl = Changelist(auth_config=auth_config)
4286 if not cl.GetIssue():
4287 parser.error('Need to upload first')
4288
4289 if not options.patchset:
4290 options.patchset = cl.GetMostRecentPatchset()
4291 if options.patchset and options.patchset != cl.GetPatchset():
4292 print(
4293 '\nWARNING Mismatch between local config and server. Did a previous '
4294 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4295 'Continuing using\npatchset %s.\n' % options.patchset)
4296 try:
4297 jobs = fetch_try_jobs(auth_config, cl, options)
4298 except BuildbucketResponseException as ex:
4299 print 'Buildbucket error: %s' % ex
4300 return 1
4301 except Exception as e:
4302 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4303 print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
4304 e, stacktrace)
4305 return 1
4306 print_tryjobs(options, jobs)
4307 return 0
4308
4309
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004310@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004311def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004312 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004313 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004314 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004315 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004316
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004317 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004318 if args:
4319 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004320 branch = cl.GetBranch()
4321 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004322 cl = Changelist()
4323 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004324
4325 # Clear configured merge-base, if there is one.
4326 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004327 else:
4328 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004329 return 0
4330
4331
thestig@chromium.org00858c82013-12-02 23:08:03 +00004332def CMDweb(parser, args):
4333 """Opens the current CL in the web browser."""
4334 _, args = parser.parse_args(args)
4335 if args:
4336 parser.error('Unrecognized args: %s' % ' '.join(args))
4337
4338 issue_url = Changelist().GetIssueURL()
4339 if not issue_url:
4340 print >> sys.stderr, 'ERROR No issue to open'
4341 return 1
4342
4343 webbrowser.open(issue_url)
4344 return 0
4345
4346
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004347def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004348 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004349 auth.add_auth_options(parser)
4350 options, args = parser.parse_args(args)
4351 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004352 if args:
4353 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004354 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004355 props = cl.GetIssueProperties()
4356 if props.get('private'):
4357 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004358 cl.SetFlag('commit', '1')
4359 return 0
4360
4361
groby@chromium.org411034a2013-02-26 15:12:01 +00004362def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004363 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004364 auth.add_auth_options(parser)
4365 options, args = parser.parse_args(args)
4366 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004367 if args:
4368 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004369 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00004370 # Ensure there actually is an issue to close.
4371 cl.GetDescription()
4372 cl.CloseIssue()
4373 return 0
4374
4375
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004376def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004377 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004378 auth.add_auth_options(parser)
4379 options, args = parser.parse_args(args)
4380 auth_config = auth.extract_auth_config_from_options(options)
4381 if args:
4382 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004383
4384 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004385 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004386 # Staged changes would be committed along with the patch from last
4387 # upload, hence counted toward the "last upload" side in the final
4388 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004389 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004390 return 1
4391
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004392 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004393 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004394 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004395 if not issue:
4396 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004397 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004398 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004399
4400 # Create a new branch based on the merge-base
4401 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004402 # Clear cached branch in cl object, to avoid overwriting original CL branch
4403 # properties.
4404 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004405 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004406 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004407 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004408 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004409 return rtn
4410
wychen@chromium.org06928532015-02-03 02:11:29 +00004411 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004412 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00004413 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004414 finally:
4415 RunGit(['checkout', '-q', branch])
4416 RunGit(['branch', '-D', TMP_BRANCH])
4417
4418 return 0
4419
4420
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004421def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004422 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004423 parser.add_option(
4424 '--no-color',
4425 action='store_true',
4426 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004427 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004428 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004429 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004430
4431 author = RunGit(['config', 'user.email']).strip() or None
4432
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004433 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004434
4435 if args:
4436 if len(args) > 1:
4437 parser.error('Unknown args')
4438 base_branch = args[0]
4439 else:
4440 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004441 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004442
4443 change = cl.GetChange(base_branch, None)
4444 return owners_finder.OwnersFinder(
4445 [f.LocalPath() for f in
4446 cl.GetChange(base_branch, None).AffectedFiles()],
4447 change.RepositoryRoot(), author,
4448 fopen=file, os_path=os.path, glob=glob.glob,
4449 disable_color=options.no_color).run()
4450
4451
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004452def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004453 """Generates a diff command."""
4454 # Generate diff for the current branch's changes.
4455 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4456 upstream_commit, '--' ]
4457
4458 if args:
4459 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004460 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004461 diff_cmd.append(arg)
4462 else:
4463 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004464
4465 return diff_cmd
4466
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004467def MatchingFileType(file_name, extensions):
4468 """Returns true if the file name ends with one of the given extensions."""
4469 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004470
enne@chromium.org555cfe42014-01-29 18:21:39 +00004471@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004472def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004473 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00004474 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004475 GN_EXTS = ['.gn', '.gni']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004476 parser.add_option('--full', action='store_true',
4477 help='Reformat the full content of all touched files')
4478 parser.add_option('--dry-run', action='store_true',
4479 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004480 parser.add_option('--python', action='store_true',
4481 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004482 parser.add_option('--diff', action='store_true',
4483 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004484 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004485
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004486 # git diff generates paths against the root of the repository. Change
4487 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004488 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004489 if rel_base_path:
4490 os.chdir(rel_base_path)
4491
digit@chromium.org29e47272013-05-17 17:01:46 +00004492 # Grab the merge-base commit, i.e. the upstream commit of the current
4493 # branch when it was created or the last time it was rebased. This is
4494 # to cover the case where the user may have called "git fetch origin",
4495 # moving the origin branch to a newer commit, but hasn't rebased yet.
4496 upstream_commit = None
4497 cl = Changelist()
4498 upstream_branch = cl.GetUpstreamBranch()
4499 if upstream_branch:
4500 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4501 upstream_commit = upstream_commit.strip()
4502
4503 if not upstream_commit:
4504 DieWithError('Could not find base commit for this branch. '
4505 'Are you in detached state?')
4506
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004507 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4508 diff_output = RunGit(changed_files_cmd)
4509 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004510 # Filter out files deleted by this CL
4511 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004512
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004513 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4514 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4515 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004516 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004517
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004518 top_dir = os.path.normpath(
4519 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
4520
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004521 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4522 # formatted. This is used to block during the presubmit.
4523 return_value = 0
4524
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004525 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004526 # Locate the clang-format binary in the checkout
4527 try:
4528 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
4529 except clang_format.NotFoundError, e:
4530 DieWithError(e)
4531
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004532 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004533 cmd = [clang_format_tool]
4534 if not opts.dry_run and not opts.diff:
4535 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004536 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004537 if opts.diff:
4538 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004539 else:
4540 env = os.environ.copy()
4541 env['PATH'] = str(os.path.dirname(clang_format_tool))
4542 try:
4543 script = clang_format.FindClangFormatScriptInChromiumTree(
4544 'clang-format-diff.py')
4545 except clang_format.NotFoundError, e:
4546 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004547
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004548 cmd = [sys.executable, script, '-p0']
4549 if not opts.dry_run and not opts.diff:
4550 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004551
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004552 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
4553 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004554
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004555 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
4556 if opts.diff:
4557 sys.stdout.write(stdout)
4558 if opts.dry_run and len(stdout) > 0:
4559 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004560
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004561 # Similar code to above, but using yapf on .py files rather than clang-format
4562 # on C/C++ files
4563 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004564 yapf_tool = gclient_utils.FindExecutable('yapf')
4565 if yapf_tool is None:
4566 DieWithError('yapf not found in PATH')
4567
4568 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004569 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004570 cmd = [yapf_tool]
4571 if not opts.dry_run and not opts.diff:
4572 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004573 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004574 if opts.diff:
4575 sys.stdout.write(stdout)
4576 else:
4577 # TODO(sbc): yapf --lines mode still has some issues.
4578 # https://github.com/google/yapf/issues/154
4579 DieWithError('--python currently only works with --full')
4580
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004581 # Dart's formatter does not have the nice property of only operating on
4582 # modified chunks, so hard code full.
4583 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004584 try:
4585 command = [dart_format.FindDartFmtToolInChromiumTree()]
4586 if not opts.dry_run and not opts.diff:
4587 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004588 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004589
ppi@chromium.org6593d932016-03-03 15:41:15 +00004590 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004591 if opts.dry_run and stdout:
4592 return_value = 2
4593 except dart_format.NotFoundError as e:
erikcorry@chromium.org3e445022015-12-17 09:07:26 +00004594 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
4595 'found in this checkout. Files in other languages are still ' +
4596 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004597
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004598 # Format GN build files. Always run on full build files for canonical form.
4599 if gn_diff_files:
4600 cmd = ['gn', 'format']
4601 if not opts.dry_run and not opts.diff:
4602 cmd.append('--in-place')
4603 for gn_diff_file in gn_diff_files:
4604 stdout = RunCommand(cmd + [gn_diff_file], cwd=top_dir)
4605 if opts.diff:
4606 sys.stdout.write(stdout)
4607
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004608 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004609
4610
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004611@subcommand.usage('<codereview url or issue id>')
4612def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004613 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004614 _, args = parser.parse_args(args)
4615
4616 if len(args) != 1:
4617 parser.print_help()
4618 return 1
4619
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004620 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00004621 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004622 parser.print_help()
4623 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00004624 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004625
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004626 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00004627 output = RunGit(['config', '--local', '--get-regexp',
4628 r'branch\..*\.%s' % issueprefix],
4629 error_ok=True)
4630 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004631 if issue == target_issue:
4632 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004633
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004634 branches = []
4635 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00004636 branches.extend(find_issues(cls.IssueSettingSuffix()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004637 if len(branches) == 0:
4638 print 'No branch found for issue %s.' % target_issue
4639 return 1
4640 if len(branches) == 1:
4641 RunGit(['checkout', branches[0]])
4642 else:
4643 print 'Multiple branches match issue %s:' % target_issue
4644 for i in range(len(branches)):
4645 print '%d: %s' % (i, branches[i])
4646 which = raw_input('Choose by index: ')
4647 try:
4648 RunGit(['checkout', branches[int(which)]])
4649 except (IndexError, ValueError):
4650 print 'Invalid selection, not checking out any branch.'
4651 return 1
4652
4653 return 0
4654
4655
maruel@chromium.org29404b52014-09-08 22:58:00 +00004656def CMDlol(parser, args):
4657 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00004658 print zlib.decompress(base64.b64decode(
4659 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
4660 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
4661 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
4662 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00004663 return 0
4664
4665
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004666class OptionParser(optparse.OptionParser):
4667 """Creates the option parse and add --verbose support."""
4668 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004669 optparse.OptionParser.__init__(
4670 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004671 self.add_option(
4672 '-v', '--verbose', action='count', default=0,
4673 help='Use 2 times for more debugging info')
4674
4675 def parse_args(self, args=None, values=None):
4676 options, args = optparse.OptionParser.parse_args(self, args, values)
4677 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
4678 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
4679 return options, args
4680
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004681
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004682def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00004683 if sys.hexversion < 0x02060000:
4684 print >> sys.stderr, (
4685 '\nYour python version %s is unsupported, please upgrade.\n' %
4686 sys.version.split(' ', 1)[0])
4687 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004688
maruel@chromium.orgddd59412011-11-30 14:20:38 +00004689 # Reload settings.
4690 global settings
4691 settings = Settings()
4692
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004693 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004694 dispatcher = subcommand.CommandDispatcher(__name__)
4695 try:
4696 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00004697 except auth.AuthenticationError as e:
4698 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004699 except urllib2.HTTPError, e:
4700 if e.code != 500:
4701 raise
4702 DieWithError(
4703 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
4704 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00004705 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004706
4707
4708if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004709 # These affect sys.stdout so do it outside of main() to simplify mocks in
4710 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00004711 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004712 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00004713 try:
4714 sys.exit(main(sys.argv[1:]))
4715 except KeyboardInterrupt:
4716 sys.stderr.write('interrupted\n')
4717 sys.exit(1)