blob: 35785ebdf0443e4c86784f4c820b0a7c71ca9103 [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
calamity@chromium.orgcf197482016-04-29 20:15:53 +000018import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import optparse
20import os
21import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000022import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000023import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024import textwrap
sheyang@google.com6ebaf782015-05-12 19:17:54 +000025import time
26import traceback
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000027import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000028import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000029import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000030import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000031import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000032import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000033
34try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000035 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000036except ImportError:
37 pass
38
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000039from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000040from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000041from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000042import auth
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +000043from luci_hacks import trigger_luci_job as luci_trigger
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000044import clang_format
tandrii@chromium.org71184c02016-01-13 15:18:44 +000045import commit_queue
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000046import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000047import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000048import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000049import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000050import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000051import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000052import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000053import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000054import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000055import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000056import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000057import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000058import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000059import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000060import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000061import watchlists
62
maruel@chromium.org0633fb42013-08-16 20:06:14 +000063__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000064
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000065DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000066POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000067DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000068GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
rmistry@google.comc68112d2015-03-03 12:48:06 +000069REFS_THAT_ALIAS_TO_OTHER_REFS = {
70 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
71 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
72}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000073
thestig@chromium.org44202a22014-03-11 19:22:18 +000074# Valid extensions for files we want to lint.
75DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
76DEFAULT_LINT_IGNORE_REGEX = r"$^"
77
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000078# Shortcut since it quickly becomes redundant.
79Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000080
maruel@chromium.orgddd59412011-11-30 14:20:38 +000081# Initialized in main()
82settings = None
83
84
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000085def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000086 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000087 sys.exit(1)
88
89
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000090def GetNoGitPagerEnv():
91 env = os.environ.copy()
92 # 'cat' is a magical git string that disables pagers on all platforms.
93 env['GIT_PAGER'] = 'cat'
94 return env
95
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000096
bsep@chromium.org627d9002016-04-29 00:00:52 +000097def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000098 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +000099 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000100 except subprocess2.CalledProcessError as e:
101 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000102 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000103 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000104 'Command "%s" failed.\n%s' % (
105 ' '.join(args), error_message or e.stdout or ''))
106 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000107
108
109def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000110 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000111 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000112
113
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000114def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000115 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000116 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000117 if suppress_stderr:
118 stderr = subprocess2.VOID
119 else:
120 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000121 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000122 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000123 stdout=subprocess2.PIPE,
124 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000125 return code, out[0]
126 except ValueError:
127 # When the subprocess fails, it returns None. That triggers a ValueError
128 # when trying to unpack the return value into (out, code).
129 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000130
131
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000132def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000133 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000134 return RunGitWithCode(args, suppress_stderr=True)[1]
135
136
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000137def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000138 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000139 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000140 return (version.startswith(prefix) and
141 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000142
143
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000144def BranchExists(branch):
145 """Return True if specified branch exists."""
146 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
147 suppress_stderr=True)
148 return not code
149
150
maruel@chromium.org90541732011-04-01 17:54:18 +0000151def ask_for_data(prompt):
152 try:
153 return raw_input(prompt)
154 except KeyboardInterrupt:
155 # Hide the exception.
156 sys.exit(1)
157
158
iannucci@chromium.org79540052012-10-19 23:15:26 +0000159def git_set_branch_value(key, value):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000160 branch = GetCurrentBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000161 if not branch:
162 return
163
164 cmd = ['config']
165 if isinstance(value, int):
166 cmd.append('--int')
167 git_key = 'branch.%s.%s' % (branch, key)
168 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000169
170
171def git_get_branch_default(key, default):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000172 branch = GetCurrentBranch()
iannucci@chromium.org79540052012-10-19 23:15:26 +0000173 if branch:
174 git_key = 'branch.%s.%s' % (branch, key)
175 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
176 try:
177 return int(stdout.strip())
178 except ValueError:
179 pass
180 return default
181
182
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000183def add_git_similarity(parser):
184 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000185 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000186 help='Sets the percentage that a pair of files need to match in order to'
187 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000188 parser.add_option(
189 '--find-copies', action='store_true',
190 help='Allows git to look for copies.')
191 parser.add_option(
192 '--no-find-copies', action='store_false', dest='find_copies',
193 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000194
195 old_parser_args = parser.parse_args
196 def Parse(args):
197 options, args = old_parser_args(args)
198
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000199 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000200 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000201 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000202 print('Note: Saving similarity of %d%% in git config.'
203 % options.similarity)
204 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000205
iannucci@chromium.org79540052012-10-19 23:15:26 +0000206 options.similarity = max(0, min(options.similarity, 100))
207
208 if options.find_copies is None:
209 options.find_copies = bool(
210 git_get_branch_default('git-find-copies', True))
211 else:
212 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000213
214 print('Using %d%% similarity for rename/copy detection. '
215 'Override with --similarity.' % options.similarity)
216
217 return options, args
218 parser.parse_args = Parse
219
220
machenbach@chromium.org45453142015-09-15 08:45:22 +0000221def _get_properties_from_options(options):
222 properties = dict(x.split('=', 1) for x in options.properties)
223 for key, val in properties.iteritems():
224 try:
225 properties[key] = json.loads(val)
226 except ValueError:
227 pass # If a value couldn't be evaluated, treat it as a string.
228 return properties
229
230
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000231def _prefix_master(master):
232 """Convert user-specified master name to full master name.
233
234 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
235 name, while the developers always use shortened master name
236 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
237 function does the conversion for buildbucket migration.
238 """
239 prefix = 'master.'
240 if master.startswith(prefix):
241 return master
242 return '%s%s' % (prefix, master)
243
244
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000245def _buildbucket_retry(operation_name, http, *args, **kwargs):
246 """Retries requests to buildbucket service and returns parsed json content."""
247 try_count = 0
248 while True:
249 response, content = http.request(*args, **kwargs)
250 try:
251 content_json = json.loads(content)
252 except ValueError:
253 content_json = None
254
255 # Buildbucket could return an error even if status==200.
256 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000257 error = content_json.get('error')
258 if error.get('code') == 403:
259 raise BuildbucketResponseException(
260 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000261 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000262 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000263 raise BuildbucketResponseException(msg)
264
265 if response.status == 200:
266 if not content_json:
267 raise BuildbucketResponseException(
268 'Buildbucket returns invalid json content: %s.\n'
269 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
270 content)
271 return content_json
272 if response.status < 500 or try_count >= 2:
273 raise httplib2.HttpLib2Error(content)
274
275 # status >= 500 means transient failures.
276 logging.debug('Transient errors when %s. Will retry.', operation_name)
277 time.sleep(0.5 + 1.5*try_count)
278 try_count += 1
279 assert False, 'unreachable'
280
281
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000282def trigger_luci_job(changelist, masters, options):
283 """Send a job to run on LUCI."""
284 issue_props = changelist.GetIssueProperties()
285 issue = changelist.GetIssue()
286 patchset = changelist.GetMostRecentPatchset()
287 for builders_and_tests in sorted(masters.itervalues()):
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000288 # TODO(hinoka et al): add support for other properties.
289 # Currently, this completely ignores testfilter and other properties.
290 for builder in sorted(builders_and_tests):
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000291 luci_trigger.trigger(
292 builder, 'HEAD', issue, patchset, issue_props['project'])
293
294
machenbach@chromium.org45453142015-09-15 08:45:22 +0000295def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000296 rietveld_url = settings.GetDefaultServerUrl()
297 rietveld_host = urlparse.urlparse(rietveld_url).hostname
298 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
299 http = authenticator.authorize(httplib2.Http())
300 http.force_exception_to_status_code = True
301 issue_props = changelist.GetIssueProperties()
302 issue = changelist.GetIssue()
303 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000304 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000305
306 buildbucket_put_url = (
307 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000308 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000309 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
310 hostname=rietveld_host,
311 issue=issue,
312 patch=patchset)
313
314 batch_req_body = {'builds': []}
315 print_text = []
316 print_text.append('Tried jobs on:')
317 for master, builders_and_tests in sorted(masters.iteritems()):
318 print_text.append('Master: %s' % master)
319 bucket = _prefix_master(master)
320 for builder, tests in sorted(builders_and_tests.iteritems()):
321 print_text.append(' %s: %s' % (builder, tests))
322 parameters = {
323 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000324 'changes': [{
325 'author': {'email': issue_props['owner_email']},
326 'revision': options.revision,
327 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000328 'properties': {
329 'category': category,
330 'issue': issue,
331 'master': master,
332 'patch_project': issue_props['project'],
333 'patch_storage': 'rietveld',
334 'patchset': patchset,
335 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000336 'rietveld': rietveld_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000337 },
338 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000339 if 'presubmit' in builder.lower():
340 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000341 if tests:
342 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000343 if properties:
344 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000345 if options.clobber:
346 parameters['properties']['clobber'] = True
347 batch_req_body['builds'].append(
348 {
349 'bucket': bucket,
350 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000351 'client_operation_id': str(uuid.uuid4()),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000352 'tags': ['builder:%s' % builder,
353 'buildset:%s' % buildset,
354 'master:%s' % master,
355 'user_agent:git_cl_try']
356 }
357 )
358
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000359 _buildbucket_retry(
360 'triggering tryjobs',
361 http,
362 buildbucket_put_url,
363 'PUT',
364 body=json.dumps(batch_req_body),
365 headers={'Content-Type': 'application/json'}
366 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000367 print_text.append('To see results here, run: git cl try-results')
368 print_text.append('To see results in browser, run: git cl web')
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000369 print '\n'.join(print_text)
kjellander@chromium.org44424542015-06-02 18:35:29 +0000370
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000371
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000372def fetch_try_jobs(auth_config, changelist, options):
373 """Fetches tryjobs from buildbucket.
374
375 Returns a map from build id to build info as json dictionary.
376 """
377 rietveld_url = settings.GetDefaultServerUrl()
378 rietveld_host = urlparse.urlparse(rietveld_url).hostname
379 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
380 if authenticator.has_cached_credentials():
381 http = authenticator.authorize(httplib2.Http())
382 else:
383 print ('Warning: Some results might be missing because %s' %
384 # Get the message on how to login.
385 auth.LoginRequiredError(rietveld_host).message)
386 http = httplib2.Http()
387
388 http.force_exception_to_status_code = True
389
390 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
391 hostname=rietveld_host,
392 issue=changelist.GetIssue(),
393 patch=options.patchset)
394 params = {'tag': 'buildset:%s' % buildset}
395
396 builds = {}
397 while True:
398 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
399 hostname=options.buildbucket_host,
400 params=urllib.urlencode(params))
401 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
402 for build in content.get('builds', []):
403 builds[build['id']] = build
404 if 'next_cursor' in content:
405 params['start_cursor'] = content['next_cursor']
406 else:
407 break
408 return builds
409
410
411def print_tryjobs(options, builds):
412 """Prints nicely result of fetch_try_jobs."""
413 if not builds:
414 print 'No tryjobs scheduled'
415 return
416
417 # Make a copy, because we'll be modifying builds dictionary.
418 builds = builds.copy()
419 builder_names_cache = {}
420
421 def get_builder(b):
422 try:
423 return builder_names_cache[b['id']]
424 except KeyError:
425 try:
426 parameters = json.loads(b['parameters_json'])
427 name = parameters['builder_name']
428 except (ValueError, KeyError) as error:
429 print 'WARNING: failed to get builder name for build %s: %s' % (
430 b['id'], error)
431 name = None
432 builder_names_cache[b['id']] = name
433 return name
434
435 def get_bucket(b):
436 bucket = b['bucket']
437 if bucket.startswith('master.'):
438 return bucket[len('master.'):]
439 return bucket
440
441 if options.print_master:
442 name_fmt = '%%-%ds %%-%ds' % (
443 max(len(str(get_bucket(b))) for b in builds.itervalues()),
444 max(len(str(get_builder(b))) for b in builds.itervalues()))
445 def get_name(b):
446 return name_fmt % (get_bucket(b), get_builder(b))
447 else:
448 name_fmt = '%%-%ds' % (
449 max(len(str(get_builder(b))) for b in builds.itervalues()))
450 def get_name(b):
451 return name_fmt % get_builder(b)
452
453 def sort_key(b):
454 return b['status'], b.get('result'), get_name(b), b.get('url')
455
456 def pop(title, f, color=None, **kwargs):
457 """Pop matching builds from `builds` dict and print them."""
458
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000459 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000460 colorize = str
461 else:
462 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
463
464 result = []
465 for b in builds.values():
466 if all(b.get(k) == v for k, v in kwargs.iteritems()):
467 builds.pop(b['id'])
468 result.append(b)
469 if result:
470 print colorize(title)
471 for b in sorted(result, key=sort_key):
472 print ' ', colorize('\t'.join(map(str, f(b))))
473
474 total = len(builds)
475 pop(status='COMPLETED', result='SUCCESS',
476 title='Successes:', color=Fore.GREEN,
477 f=lambda b: (get_name(b), b.get('url')))
478 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
479 title='Infra Failures:', color=Fore.MAGENTA,
480 f=lambda b: (get_name(b), b.get('url')))
481 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
482 title='Failures:', color=Fore.RED,
483 f=lambda b: (get_name(b), b.get('url')))
484 pop(status='COMPLETED', result='CANCELED',
485 title='Canceled:', color=Fore.MAGENTA,
486 f=lambda b: (get_name(b),))
487 pop(status='COMPLETED', result='FAILURE',
488 failure_reason='INVALID_BUILD_DEFINITION',
489 title='Wrong master/builder name:', color=Fore.MAGENTA,
490 f=lambda b: (get_name(b),))
491 pop(status='COMPLETED', result='FAILURE',
492 title='Other failures:',
493 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
494 pop(status='COMPLETED',
495 title='Other finished:',
496 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
497 pop(status='STARTED',
498 title='Started:', color=Fore.YELLOW,
499 f=lambda b: (get_name(b), b.get('url')))
500 pop(status='SCHEDULED',
501 title='Scheduled:',
502 f=lambda b: (get_name(b), 'id=%s' % b['id']))
503 # The last section is just in case buildbucket API changes OR there is a bug.
504 pop(title='Other:',
505 f=lambda b: (get_name(b), 'id=%s' % b['id']))
506 assert len(builds) == 0
507 print 'Total: %d tryjobs' % total
508
509
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000510def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
511 """Return the corresponding git ref if |base_url| together with |glob_spec|
512 matches the full |url|.
513
514 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
515 """
516 fetch_suburl, as_ref = glob_spec.split(':')
517 if allow_wildcards:
518 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
519 if glob_match:
520 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
521 # "branches/{472,597,648}/src:refs/remotes/svn/*".
522 branch_re = re.escape(base_url)
523 if glob_match.group(1):
524 branch_re += '/' + re.escape(glob_match.group(1))
525 wildcard = glob_match.group(2)
526 if wildcard == '*':
527 branch_re += '([^/]*)'
528 else:
529 # Escape and replace surrounding braces with parentheses and commas
530 # with pipe symbols.
531 wildcard = re.escape(wildcard)
532 wildcard = re.sub('^\\\\{', '(', wildcard)
533 wildcard = re.sub('\\\\,', '|', wildcard)
534 wildcard = re.sub('\\\\}$', ')', wildcard)
535 branch_re += wildcard
536 if glob_match.group(3):
537 branch_re += re.escape(glob_match.group(3))
538 match = re.match(branch_re, url)
539 if match:
540 return re.sub('\*$', match.group(1), as_ref)
541
542 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
543 if fetch_suburl:
544 full_url = base_url + '/' + fetch_suburl
545 else:
546 full_url = base_url
547 if full_url == url:
548 return as_ref
549 return None
550
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000551
iannucci@chromium.org79540052012-10-19 23:15:26 +0000552def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000553 """Prints statistics about the change to the user."""
554 # --no-ext-diff is broken in some versions of Git, so try to work around
555 # this by overriding the environment (but there is still a problem if the
556 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000557 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000558 if 'GIT_EXTERNAL_DIFF' in env:
559 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000560
561 if find_copies:
562 similarity_options = ['--find-copies-harder', '-l100000',
563 '-C%s' % similarity]
564 else:
565 similarity_options = ['-M%s' % similarity]
566
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000567 try:
568 stdout = sys.stdout.fileno()
569 except AttributeError:
570 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000571 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000572 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000573 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000574 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000575
576
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000577class BuildbucketResponseException(Exception):
578 pass
579
580
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000581class Settings(object):
582 def __init__(self):
583 self.default_server = None
584 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000585 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000586 self.is_git_svn = None
587 self.svn_branch = None
588 self.tree_status_url = None
589 self.viewvc_url = None
590 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000591 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000592 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000593 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000594 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000595 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000596 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000597 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000598
599 def LazyUpdateIfNeeded(self):
600 """Updates the settings from a codereview.settings file, if available."""
601 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000602 # The only value that actually changes the behavior is
603 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000604 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000605 error_ok=True
606 ).strip().lower()
607
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000608 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000609 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000610 LoadCodereviewSettingsFromFile(cr_settings_file)
611 self.updated = True
612
613 def GetDefaultServerUrl(self, error_ok=False):
614 if not self.default_server:
615 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000616 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000617 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000618 if error_ok:
619 return self.default_server
620 if not self.default_server:
621 error_message = ('Could not find settings file. You must configure '
622 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000623 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000624 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000625 return self.default_server
626
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000627 @staticmethod
628 def GetRelativeRoot():
629 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000630
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000631 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000632 if self.root is None:
633 self.root = os.path.abspath(self.GetRelativeRoot())
634 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000635
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000636 def GetGitMirror(self, remote='origin'):
637 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000638 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000639 if not os.path.isdir(local_url):
640 return None
641 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
642 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
643 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
644 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
645 if mirror.exists():
646 return mirror
647 return None
648
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000649 def GetIsGitSvn(self):
650 """Return true if this repo looks like it's using git-svn."""
651 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000652 if self.GetPendingRefPrefix():
653 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
654 self.is_git_svn = False
655 else:
656 # If you have any "svn-remote.*" config keys, we think you're using svn.
657 self.is_git_svn = RunGitWithCode(
658 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000659 return self.is_git_svn
660
661 def GetSVNBranch(self):
662 if self.svn_branch is None:
663 if not self.GetIsGitSvn():
664 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
665
666 # Try to figure out which remote branch we're based on.
667 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000668 # 1) iterate through our branch history and find the svn URL.
669 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000670
671 # regexp matching the git-svn line that contains the URL.
672 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
673
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000674 # We don't want to go through all of history, so read a line from the
675 # pipe at a time.
676 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000677 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000678 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
679 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000680 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000681 for line in proc.stdout:
682 match = git_svn_re.match(line)
683 if match:
684 url = match.group(1)
685 proc.stdout.close() # Cut pipe.
686 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000687
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000688 if url:
689 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
690 remotes = RunGit(['config', '--get-regexp',
691 r'^svn-remote\..*\.url']).splitlines()
692 for remote in remotes:
693 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000694 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000695 remote = match.group(1)
696 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000697 rewrite_root = RunGit(
698 ['config', 'svn-remote.%s.rewriteRoot' % remote],
699 error_ok=True).strip()
700 if rewrite_root:
701 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000702 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000703 ['config', 'svn-remote.%s.fetch' % remote],
704 error_ok=True).strip()
705 if fetch_spec:
706 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
707 if self.svn_branch:
708 break
709 branch_spec = RunGit(
710 ['config', 'svn-remote.%s.branches' % remote],
711 error_ok=True).strip()
712 if branch_spec:
713 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
714 if self.svn_branch:
715 break
716 tag_spec = RunGit(
717 ['config', 'svn-remote.%s.tags' % remote],
718 error_ok=True).strip()
719 if tag_spec:
720 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
721 if self.svn_branch:
722 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000723
724 if not self.svn_branch:
725 DieWithError('Can\'t guess svn branch -- try specifying it on the '
726 'command line')
727
728 return self.svn_branch
729
730 def GetTreeStatusUrl(self, error_ok=False):
731 if not self.tree_status_url:
732 error_message = ('You must configure your tree status URL by running '
733 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000734 self.tree_status_url = self._GetRietveldConfig(
735 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000736 return self.tree_status_url
737
738 def GetViewVCUrl(self):
739 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000740 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000741 return self.viewvc_url
742
rmistry@google.com90752582014-01-14 21:04:50 +0000743 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000744 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000745
rmistry@google.com78948ed2015-07-08 23:09:57 +0000746 def GetIsSkipDependencyUpload(self, branch_name):
747 """Returns true if specified branch should skip dep uploads."""
748 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
749 error_ok=True)
750
rmistry@google.com5626a922015-02-26 14:03:30 +0000751 def GetRunPostUploadHook(self):
752 run_post_upload_hook = self._GetRietveldConfig(
753 'run-post-upload-hook', error_ok=True)
754 return run_post_upload_hook == "True"
755
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000756 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000757 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000758
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000759 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000760 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000761
ukai@chromium.orge8077812012-02-03 03:41:46 +0000762 def GetIsGerrit(self):
763 """Return true if this repo is assosiated with gerrit code review system."""
764 if self.is_gerrit is None:
765 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
766 return self.is_gerrit
767
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000768 def GetSquashGerritUploads(self):
769 """Return true if uploads to Gerrit should be squashed by default."""
770 if self.squash_gerrit_uploads is None:
771 self.squash_gerrit_uploads = (
772 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
773 error_ok=True).strip() == 'true')
774 return self.squash_gerrit_uploads
775
tandrii@chromium.org28253532016-04-14 13:46:56 +0000776 def GetGerritSkipEnsureAuthenticated(self):
777 """Return True if EnsureAuthenticated should not be done for Gerrit
778 uploads."""
779 if self.gerrit_skip_ensure_authenticated is None:
780 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000781 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000782 error_ok=True).strip() == 'true')
783 return self.gerrit_skip_ensure_authenticated
784
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000785 def GetGitEditor(self):
786 """Return the editor specified in the git config, or None if none is."""
787 if self.git_editor is None:
788 self.git_editor = self._GetConfig('core.editor', error_ok=True)
789 return self.git_editor or None
790
thestig@chromium.org44202a22014-03-11 19:22:18 +0000791 def GetLintRegex(self):
792 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
793 DEFAULT_LINT_REGEX)
794
795 def GetLintIgnoreRegex(self):
796 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
797 DEFAULT_LINT_IGNORE_REGEX)
798
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000799 def GetProject(self):
800 if not self.project:
801 self.project = self._GetRietveldConfig('project', error_ok=True)
802 return self.project
803
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000804 def GetForceHttpsCommitUrl(self):
805 if not self.force_https_commit_url:
806 self.force_https_commit_url = self._GetRietveldConfig(
807 'force-https-commit-url', error_ok=True)
808 return self.force_https_commit_url
809
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000810 def GetPendingRefPrefix(self):
811 if not self.pending_ref_prefix:
812 self.pending_ref_prefix = self._GetRietveldConfig(
813 'pending-ref-prefix', error_ok=True)
814 return self.pending_ref_prefix
815
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000816 def _GetRietveldConfig(self, param, **kwargs):
817 return self._GetConfig('rietveld.' + param, **kwargs)
818
rmistry@google.com78948ed2015-07-08 23:09:57 +0000819 def _GetBranchConfig(self, branch_name, param, **kwargs):
820 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
821
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000822 def _GetConfig(self, param, **kwargs):
823 self.LazyUpdateIfNeeded()
824 return RunGit(['config', param], **kwargs).strip()
825
826
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000827def ShortBranchName(branch):
828 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000829 return branch.replace('refs/heads/', '', 1)
830
831
832def GetCurrentBranchRef():
833 """Returns branch ref (e.g., refs/heads/master) or None."""
834 return RunGit(['symbolic-ref', 'HEAD'],
835 stderr=subprocess2.VOID, error_ok=True).strip() or None
836
837
838def GetCurrentBranch():
839 """Returns current branch or None.
840
841 For refs/heads/* branches, returns just last part. For others, full ref.
842 """
843 branchref = GetCurrentBranchRef()
844 if branchref:
845 return ShortBranchName(branchref)
846 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000847
848
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000849class _CQState(object):
850 """Enum for states of CL with respect to Commit Queue."""
851 NONE = 'none'
852 DRY_RUN = 'dry_run'
853 COMMIT = 'commit'
854
855 ALL_STATES = [NONE, DRY_RUN, COMMIT]
856
857
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000858class _ParsedIssueNumberArgument(object):
859 def __init__(self, issue=None, patchset=None, hostname=None):
860 self.issue = issue
861 self.patchset = patchset
862 self.hostname = hostname
863
864 @property
865 def valid(self):
866 return self.issue is not None
867
868
869class _RietveldParsedIssueNumberArgument(_ParsedIssueNumberArgument):
870 def __init__(self, *args, **kwargs):
871 self.patch_url = kwargs.pop('patch_url', None)
872 super(_RietveldParsedIssueNumberArgument, self).__init__(*args, **kwargs)
873
874
875def ParseIssueNumberArgument(arg):
876 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
877 fail_result = _ParsedIssueNumberArgument()
878
879 if arg.isdigit():
880 return _ParsedIssueNumberArgument(issue=int(arg))
881 if not arg.startswith('http'):
882 return fail_result
883 url = gclient_utils.UpgradeToHttps(arg)
884 try:
885 parsed_url = urlparse.urlparse(url)
886 except ValueError:
887 return fail_result
888 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
889 tmp = cls.ParseIssueURL(parsed_url)
890 if tmp is not None:
891 return tmp
892 return fail_result
893
894
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000895class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000896 """Changelist works with one changelist in local branch.
897
898 Supports two codereview backends: Rietveld or Gerrit, selected at object
899 creation.
900
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +0000901 Notes:
902 * Not safe for concurrent multi-{thread,process} use.
903 * Caches values from current branch. Therefore, re-use after branch change
904 with care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000905 """
906
907 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
908 """Create a new ChangeList instance.
909
910 If issue is given, the codereview must be given too.
911
912 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
913 Otherwise, it's decided based on current configuration of the local branch,
914 with default being 'rietveld' for backwards compatibility.
915 See _load_codereview_impl for more details.
916
917 **kwargs will be passed directly to codereview implementation.
918 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000919 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000920 global settings
921 if not settings:
922 # Happens when git_cl.py is used as a utility library.
923 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000924
925 if issue:
926 assert codereview, 'codereview must be known, if issue is known'
927
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000928 self.branchref = branchref
929 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000930 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000931 self.branch = ShortBranchName(self.branchref)
932 else:
933 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000934 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000935 self.lookedup_issue = False
936 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000937 self.has_description = False
938 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000939 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000940 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000941 self.cc = None
942 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000943 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000944
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000945 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000946 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000947 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000948 assert self._codereview_impl
949 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000950
951 def _load_codereview_impl(self, codereview=None, **kwargs):
952 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000953 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
954 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
955 self._codereview = codereview
956 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000957 return
958
959 # Automatic selection based on issue number set for a current branch.
960 # Rietveld takes precedence over Gerrit.
961 assert not self.issue
962 # Whether we find issue or not, we are doing the lookup.
963 self.lookedup_issue = True
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000964 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000965 setting = cls.IssueSetting(self.GetBranch())
966 issue = RunGit(['config', setting], error_ok=True).strip()
967 if issue:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000968 self._codereview = codereview
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000969 self._codereview_impl = cls(self, **kwargs)
970 self.issue = int(issue)
971 return
972
973 # No issue is set for this branch, so decide based on repo-wide settings.
974 return self._load_codereview_impl(
975 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
976 **kwargs)
977
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000978 def IsGerrit(self):
979 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000980
981 def GetCCList(self):
982 """Return the users cc'd on this CL.
983
984 Return is a string suitable for passing to gcl with the --cc flag.
985 """
986 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000987 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000988 more_cc = ','.join(self.watchers)
989 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
990 return self.cc
991
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000992 def GetCCListWithoutDefault(self):
993 """Return the users cc'd on this CL excluding default ones."""
994 if self.cc is None:
995 self.cc = ','.join(self.watchers)
996 return self.cc
997
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000998 def SetWatchers(self, watchers):
999 """Set the list of email addresses that should be cc'd based on the changed
1000 files in this CL.
1001 """
1002 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001003
1004 def GetBranch(self):
1005 """Returns the short branch name, e.g. 'master'."""
1006 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001007 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001008 if not branchref:
1009 return None
1010 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001011 self.branch = ShortBranchName(self.branchref)
1012 return self.branch
1013
1014 def GetBranchRef(self):
1015 """Returns the full branch name, e.g. 'refs/heads/master'."""
1016 self.GetBranch() # Poke the lazy loader.
1017 return self.branchref
1018
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001019 def ClearBranch(self):
1020 """Clears cached branch data of this object."""
1021 self.branch = self.branchref = None
1022
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001023 @staticmethod
1024 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001025 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001026 e.g. 'origin', 'refs/heads/master'
1027 """
1028 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001029 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
1030 error_ok=True).strip()
1031 if upstream_branch:
1032 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
1033 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001034 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1035 error_ok=True).strip()
1036 if upstream_branch:
1037 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001038 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001039 # Fall back on trying a git-svn upstream branch.
1040 if settings.GetIsGitSvn():
1041 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001042 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001043 # Else, try to guess the origin remote.
1044 remote_branches = RunGit(['branch', '-r']).split()
1045 if 'origin/master' in remote_branches:
1046 # Fall back on origin/master if it exits.
1047 remote = 'origin'
1048 upstream_branch = 'refs/heads/master'
1049 elif 'origin/trunk' in remote_branches:
1050 # Fall back on origin/trunk if it exists. Generally a shared
1051 # git-svn clone
1052 remote = 'origin'
1053 upstream_branch = 'refs/heads/trunk'
1054 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001055 DieWithError(
1056 'Unable to determine default branch to diff against.\n'
1057 'Either pass complete "git diff"-style arguments, like\n'
1058 ' git cl upload origin/master\n'
1059 'or verify this branch is set up to track another \n'
1060 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001061
1062 return remote, upstream_branch
1063
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001064 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001065 upstream_branch = self.GetUpstreamBranch()
1066 if not BranchExists(upstream_branch):
1067 DieWithError('The upstream for the current branch (%s) does not exist '
1068 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001069 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001070 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001071
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001072 def GetUpstreamBranch(self):
1073 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001074 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001075 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001076 upstream_branch = upstream_branch.replace('refs/heads/',
1077 'refs/remotes/%s/' % remote)
1078 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1079 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001080 self.upstream_branch = upstream_branch
1081 return self.upstream_branch
1082
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001083 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001084 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001085 remote, branch = None, self.GetBranch()
1086 seen_branches = set()
1087 while branch not in seen_branches:
1088 seen_branches.add(branch)
1089 remote, branch = self.FetchUpstreamTuple(branch)
1090 branch = ShortBranchName(branch)
1091 if remote != '.' or branch.startswith('refs/remotes'):
1092 break
1093 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001094 remotes = RunGit(['remote'], error_ok=True).split()
1095 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001096 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001097 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001098 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001099 logging.warning('Could not determine which remote this change is '
1100 'associated with, so defaulting to "%s". This may '
1101 'not be what you want. You may prevent this message '
1102 'by running "git svn info" as documented here: %s',
1103 self._remote,
1104 GIT_INSTRUCTIONS_URL)
1105 else:
1106 logging.warn('Could not determine which remote this change is '
1107 'associated with. You may prevent this message by '
1108 'running "git svn info" as documented here: %s',
1109 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001110 branch = 'HEAD'
1111 if branch.startswith('refs/remotes'):
1112 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001113 elif branch.startswith('refs/branch-heads/'):
1114 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001115 else:
1116 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001117 return self._remote
1118
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001119 def GitSanityChecks(self, upstream_git_obj):
1120 """Checks git repo status and ensures diff is from local commits."""
1121
sbc@chromium.org79706062015-01-14 21:18:12 +00001122 if upstream_git_obj is None:
1123 if self.GetBranch() is None:
1124 print >> sys.stderr, (
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00001125 'ERROR: unable to determine current branch (detached HEAD?)')
sbc@chromium.org79706062015-01-14 21:18:12 +00001126 else:
1127 print >> sys.stderr, (
1128 'ERROR: no upstream branch')
1129 return False
1130
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001131 # Verify the commit we're diffing against is in our current branch.
1132 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1133 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1134 if upstream_sha != common_ancestor:
1135 print >> sys.stderr, (
1136 'ERROR: %s is not in the current branch. You may need to rebase '
1137 'your tracking branch' % upstream_sha)
1138 return False
1139
1140 # List the commits inside the diff, and verify they are all local.
1141 commits_in_diff = RunGit(
1142 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1143 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1144 remote_branch = remote_branch.strip()
1145 if code != 0:
1146 _, remote_branch = self.GetRemoteBranch()
1147
1148 commits_in_remote = RunGit(
1149 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1150
1151 common_commits = set(commits_in_diff) & set(commits_in_remote)
1152 if common_commits:
1153 print >> sys.stderr, (
1154 'ERROR: Your diff contains %d commits already in %s.\n'
1155 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1156 'the diff. If you are using a custom git flow, you can override'
1157 ' the reference used for this check with "git config '
1158 'gitcl.remotebranch <git-ref>".' % (
1159 len(common_commits), remote_branch, upstream_git_obj))
1160 return False
1161 return True
1162
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001163 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001164 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001165
1166 Returns None if it is not set.
1167 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001168 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
1169 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001170
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001171 def GetGitSvnRemoteUrl(self):
1172 """Return the configured git-svn remote URL parsed from git svn info.
1173
1174 Returns None if it is not set.
1175 """
1176 # URL is dependent on the current directory.
1177 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1178 if data:
1179 keys = dict(line.split(': ', 1) for line in data.splitlines()
1180 if ': ' in line)
1181 return keys.get('URL', None)
1182 return None
1183
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001184 def GetRemoteUrl(self):
1185 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1186
1187 Returns None if there is no remote.
1188 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001189 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001190 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1191
1192 # If URL is pointing to a local directory, it is probably a git cache.
1193 if os.path.isdir(url):
1194 url = RunGit(['config', 'remote.%s.url' % remote],
1195 error_ok=True,
1196 cwd=url).strip()
1197 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001198
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001199 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001200 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001201 if self.issue is None and not self.lookedup_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001202 issue = RunGit(['config',
1203 self._codereview_impl.IssueSetting(self.GetBranch())],
1204 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001205 self.issue = int(issue) or None if issue else None
1206 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001207 return self.issue
1208
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001209 def GetIssueURL(self):
1210 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001211 issue = self.GetIssue()
1212 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001213 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001214 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001215
1216 def GetDescription(self, pretty=False):
1217 if not self.has_description:
1218 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001219 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001220 self.has_description = True
1221 if pretty:
1222 wrapper = textwrap.TextWrapper()
1223 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1224 return wrapper.fill(self.description)
1225 return self.description
1226
1227 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001228 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001229 if self.patchset is None and not self.lookedup_patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001230 patchset = RunGit(['config', self._codereview_impl.PatchsetSetting()],
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001231 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001232 self.patchset = int(patchset) or None if patchset else None
1233 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001234 return self.patchset
1235
1236 def SetPatchset(self, patchset):
1237 """Set this branch's patchset. If patchset=0, clears the patchset."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001238 patchset_setting = self._codereview_impl.PatchsetSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001239 if patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001240 RunGit(['config', patchset_setting, str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001241 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001242 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001243 RunGit(['config', '--unset', patchset_setting],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001244 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001245 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001246
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001247 def SetIssue(self, issue=None):
1248 """Set this branch's issue. If issue isn't given, clears the issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001249 issue_setting = self._codereview_impl.IssueSetting(self.GetBranch())
1250 codereview_setting = self._codereview_impl.GetCodereviewServerSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001251 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001252 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001253 RunGit(['config', issue_setting, str(issue)])
1254 codereview_server = self._codereview_impl.GetCodereviewServer()
1255 if codereview_server:
1256 RunGit(['config', codereview_setting, codereview_server])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001257 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +00001258 current_issue = self.GetIssue()
1259 if current_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001260 RunGit(['config', '--unset', issue_setting])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001261 self.issue = None
1262 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001263
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001264 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001265 if not self.GitSanityChecks(upstream_branch):
1266 DieWithError('\nGit sanity check failure')
1267
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001268 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001269 if not root:
1270 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001271 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001272
1273 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001274 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001275 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001276 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001277 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001278 except subprocess2.CalledProcessError:
1279 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001280 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001281 'This branch probably doesn\'t exist anymore. To reset the\n'
1282 'tracking branch, please run\n'
1283 ' git branch --set-upstream %s trunk\n'
1284 'replacing trunk with origin/master or the relevant branch') %
1285 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001286
maruel@chromium.org52424302012-08-29 15:14:30 +00001287 issue = self.GetIssue()
1288 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001289 if issue:
1290 description = self.GetDescription()
1291 else:
1292 # If the change was never uploaded, use the log messages of all commits
1293 # up to the branch point, as git cl upload will prefill the description
1294 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001295 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1296 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001297
1298 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001299 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001300 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001301 name,
1302 description,
1303 absroot,
1304 files,
1305 issue,
1306 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001307 author,
1308 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001309
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001310 def UpdateDescription(self, description):
1311 self.description = description
1312 return self._codereview_impl.UpdateDescriptionRemote(description)
1313
1314 def RunHook(self, committing, may_prompt, verbose, change):
1315 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1316 try:
1317 return presubmit_support.DoPresubmitChecks(change, committing,
1318 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1319 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001320 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1321 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001322 except presubmit_support.PresubmitFailure, e:
1323 DieWithError(
1324 ('%s\nMaybe your depot_tools is out of date?\n'
1325 'If all fails, contact maruel@') % e)
1326
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001327 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1328 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001329 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1330 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001331 else:
1332 # Assume url.
1333 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1334 urlparse.urlparse(issue_arg))
1335 if not parsed_issue_arg or not parsed_issue_arg.valid:
1336 DieWithError('Failed to parse issue argument "%s". '
1337 'Must be an issue number or a valid URL.' % issue_arg)
1338 return self._codereview_impl.CMDPatchWithParsedIssue(
1339 parsed_issue_arg, reject, nocommit, directory)
1340
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001341 def CMDUpload(self, options, git_diff_args, orig_args):
1342 """Uploads a change to codereview."""
1343 if git_diff_args:
1344 # TODO(ukai): is it ok for gerrit case?
1345 base_branch = git_diff_args[0]
1346 else:
1347 if self.GetBranch() is None:
1348 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1349
1350 # Default to diffing against common ancestor of upstream branch
1351 base_branch = self.GetCommonAncestorWithUpstream()
1352 git_diff_args = [base_branch, 'HEAD']
1353
1354 # Make sure authenticated to codereview before running potentially expensive
1355 # hooks. It is a fast, best efforts check. Codereview still can reject the
1356 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001357 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001358
1359 # Apply watchlists on upload.
1360 change = self.GetChange(base_branch, None)
1361 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1362 files = [f.LocalPath() for f in change.AffectedFiles()]
1363 if not options.bypass_watchlists:
1364 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1365
1366 if not options.bypass_hooks:
1367 if options.reviewers or options.tbr_owners:
1368 # Set the reviewer list now so that presubmit checks can access it.
1369 change_description = ChangeDescription(change.FullDescriptionText())
1370 change_description.update_reviewers(options.reviewers,
1371 options.tbr_owners,
1372 change)
1373 change.SetDescriptionText(change_description.description)
1374 hook_results = self.RunHook(committing=False,
1375 may_prompt=not options.force,
1376 verbose=options.verbose,
1377 change=change)
1378 if not hook_results.should_continue():
1379 return 1
1380 if not options.reviewers and hook_results.reviewers:
1381 options.reviewers = hook_results.reviewers.split(',')
1382
1383 if self.GetIssue():
1384 latest_patchset = self.GetMostRecentPatchset()
1385 local_patchset = self.GetPatchset()
1386 if (latest_patchset and local_patchset and
1387 local_patchset != latest_patchset):
1388 print ('The last upload made from this repository was patchset #%d but '
1389 'the most recent patchset on the server is #%d.'
1390 % (local_patchset, latest_patchset))
1391 print ('Uploading will still work, but if you\'ve uploaded to this '
1392 'issue from another machine or branch the patch you\'re '
1393 'uploading now might not include those changes.')
1394 ask_for_data('About to upload; enter to confirm.')
1395
1396 print_stats(options.similarity, options.find_copies, git_diff_args)
1397 ret = self.CMDUploadChange(options, git_diff_args, change)
1398 if not ret:
1399 git_set_branch_value('last-upload-hash',
1400 RunGit(['rev-parse', 'HEAD']).strip())
1401 # Run post upload hooks, if specified.
1402 if settings.GetRunPostUploadHook():
1403 presubmit_support.DoPostUploadExecuter(
1404 change,
1405 self,
1406 settings.GetRoot(),
1407 options.verbose,
1408 sys.stdout)
1409
1410 # Upload all dependencies if specified.
1411 if options.dependencies:
1412 print
1413 print '--dependencies has been specified.'
1414 print 'All dependent local branches will be re-uploaded.'
1415 print
1416 # Remove the dependencies flag from args so that we do not end up in a
1417 # loop.
1418 orig_args.remove('--dependencies')
1419 ret = upload_branch_deps(self, orig_args)
1420 return ret
1421
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001422 def SetCQState(self, new_state):
1423 """Update the CQ state for latest patchset.
1424
1425 Issue must have been already uploaded and known.
1426 """
1427 assert new_state in _CQState.ALL_STATES
1428 assert self.GetIssue()
1429 return self._codereview_impl.SetCQState(new_state)
1430
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001431 # Forward methods to codereview specific implementation.
1432
1433 def CloseIssue(self):
1434 return self._codereview_impl.CloseIssue()
1435
1436 def GetStatus(self):
1437 return self._codereview_impl.GetStatus()
1438
1439 def GetCodereviewServer(self):
1440 return self._codereview_impl.GetCodereviewServer()
1441
1442 def GetApprovingReviewers(self):
1443 return self._codereview_impl.GetApprovingReviewers()
1444
1445 def GetMostRecentPatchset(self):
1446 return self._codereview_impl.GetMostRecentPatchset()
1447
1448 def __getattr__(self, attr):
1449 # This is because lots of untested code accesses Rietveld-specific stuff
1450 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001451 # on a case by case basis.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001452 return getattr(self._codereview_impl, attr)
1453
1454
1455class _ChangelistCodereviewBase(object):
1456 """Abstract base class encapsulating codereview specifics of a changelist."""
1457 def __init__(self, changelist):
1458 self._changelist = changelist # instance of Changelist
1459
1460 def __getattr__(self, attr):
1461 # Forward methods to changelist.
1462 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1463 # _RietveldChangelistImpl to avoid this hack?
1464 return getattr(self._changelist, attr)
1465
1466 def GetStatus(self):
1467 """Apply a rough heuristic to give a simple summary of an issue's review
1468 or CQ status, assuming adherence to a common workflow.
1469
1470 Returns None if no issue for this branch, or specific string keywords.
1471 """
1472 raise NotImplementedError()
1473
1474 def GetCodereviewServer(self):
1475 """Returns server URL without end slash, like "https://codereview.com"."""
1476 raise NotImplementedError()
1477
1478 def FetchDescription(self):
1479 """Fetches and returns description from the codereview server."""
1480 raise NotImplementedError()
1481
1482 def GetCodereviewServerSetting(self):
1483 """Returns git config setting for the codereview server."""
1484 raise NotImplementedError()
1485
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001486 @classmethod
1487 def IssueSetting(cls, branch):
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001488 return 'branch.%s.%s' % (branch, cls.IssueSettingSuffix())
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001489
1490 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001491 def IssueSettingSuffix(cls):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001492 """Returns name of git config setting which stores issue number for a given
1493 branch."""
1494 raise NotImplementedError()
1495
1496 def PatchsetSetting(self):
1497 """Returns name of git config setting which stores issue number."""
1498 raise NotImplementedError()
1499
1500 def GetRieveldObjForPresubmit(self):
1501 # This is an unfortunate Rietveld-embeddedness in presubmit.
1502 # For non-Rietveld codereviews, this probably should return a dummy object.
1503 raise NotImplementedError()
1504
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001505 def GetGerritObjForPresubmit(self):
1506 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1507 return None
1508
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001509 def UpdateDescriptionRemote(self, description):
1510 """Update the description on codereview site."""
1511 raise NotImplementedError()
1512
1513 def CloseIssue(self):
1514 """Closes the issue."""
1515 raise NotImplementedError()
1516
1517 def GetApprovingReviewers(self):
1518 """Returns a list of reviewers approving the change.
1519
1520 Note: not necessarily committers.
1521 """
1522 raise NotImplementedError()
1523
1524 def GetMostRecentPatchset(self):
1525 """Returns the most recent patchset number from the codereview site."""
1526 raise NotImplementedError()
1527
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001528 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1529 directory):
1530 """Fetches and applies the issue.
1531
1532 Arguments:
1533 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1534 reject: if True, reject the failed patch instead of switching to 3-way
1535 merge. Rietveld only.
1536 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1537 only.
1538 directory: switch to directory before applying the patch. Rietveld only.
1539 """
1540 raise NotImplementedError()
1541
1542 @staticmethod
1543 def ParseIssueURL(parsed_url):
1544 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1545 failed."""
1546 raise NotImplementedError()
1547
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001548 def EnsureAuthenticated(self, force):
1549 """Best effort check that user is authenticated with codereview server.
1550
1551 Arguments:
1552 force: whether to skip confirmation questions.
1553 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001554 raise NotImplementedError()
1555
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001556 def CMDUploadChange(self, options, args, change):
1557 """Uploads a change to codereview."""
1558 raise NotImplementedError()
1559
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001560 def SetCQState(self, new_state):
1561 """Update the CQ state for latest patchset.
1562
1563 Issue must have been already uploaded and known.
1564 """
1565 raise NotImplementedError()
1566
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001567
1568class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1569 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1570 super(_RietveldChangelistImpl, self).__init__(changelist)
1571 assert settings, 'must be initialized in _ChangelistCodereviewBase'
1572 settings.GetDefaultServerUrl()
1573
1574 self._rietveld_server = rietveld_server
1575 self._auth_config = auth_config
1576 self._props = None
1577 self._rpc_server = None
1578
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001579 def GetCodereviewServer(self):
1580 if not self._rietveld_server:
1581 # If we're on a branch then get the server potentially associated
1582 # with that branch.
1583 if self.GetIssue():
1584 rietveld_server_setting = self.GetCodereviewServerSetting()
1585 if rietveld_server_setting:
1586 self._rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1587 ['config', rietveld_server_setting], error_ok=True).strip())
1588 if not self._rietveld_server:
1589 self._rietveld_server = settings.GetDefaultServerUrl()
1590 return self._rietveld_server
1591
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001592 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001593 """Best effort check that user is authenticated with Rietveld server."""
1594 if self._auth_config.use_oauth2:
1595 authenticator = auth.get_authenticator_for_host(
1596 self.GetCodereviewServer(), self._auth_config)
1597 if not authenticator.has_cached_credentials():
1598 raise auth.LoginRequiredError(self.GetCodereviewServer())
1599
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001600 def FetchDescription(self):
1601 issue = self.GetIssue()
1602 assert issue
1603 try:
1604 return self.RpcServer().get_description(issue).strip()
1605 except urllib2.HTTPError as e:
1606 if e.code == 404:
1607 DieWithError(
1608 ('\nWhile fetching the description for issue %d, received a '
1609 '404 (not found)\n'
1610 'error. It is likely that you deleted this '
1611 'issue on the server. If this is the\n'
1612 'case, please run\n\n'
1613 ' git cl issue 0\n\n'
1614 'to clear the association with the deleted issue. Then run '
1615 'this command again.') % issue)
1616 else:
1617 DieWithError(
1618 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1619 except urllib2.URLError as e:
1620 print >> sys.stderr, (
1621 'Warning: Failed to retrieve CL description due to network '
1622 'failure.')
1623 return ''
1624
1625 def GetMostRecentPatchset(self):
1626 return self.GetIssueProperties()['patchsets'][-1]
1627
1628 def GetPatchSetDiff(self, issue, patchset):
1629 return self.RpcServer().get(
1630 '/download/issue%s_%s.diff' % (issue, patchset))
1631
1632 def GetIssueProperties(self):
1633 if self._props is None:
1634 issue = self.GetIssue()
1635 if not issue:
1636 self._props = {}
1637 else:
1638 self._props = self.RpcServer().get_issue_properties(issue, True)
1639 return self._props
1640
1641 def GetApprovingReviewers(self):
1642 return get_approving_reviewers(self.GetIssueProperties())
1643
1644 def AddComment(self, message):
1645 return self.RpcServer().add_comment(self.GetIssue(), message)
1646
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001647 def GetStatus(self):
1648 """Apply a rough heuristic to give a simple summary of an issue's review
1649 or CQ status, assuming adherence to a common workflow.
1650
1651 Returns None if no issue for this branch, or one of the following keywords:
1652 * 'error' - error from review tool (including deleted issues)
1653 * 'unsent' - not sent for review
1654 * 'waiting' - waiting for review
1655 * 'reply' - waiting for owner to reply to review
1656 * 'lgtm' - LGTM from at least one approved reviewer
1657 * 'commit' - in the commit queue
1658 * 'closed' - closed
1659 """
1660 if not self.GetIssue():
1661 return None
1662
1663 try:
1664 props = self.GetIssueProperties()
1665 except urllib2.HTTPError:
1666 return 'error'
1667
1668 if props.get('closed'):
1669 # Issue is closed.
1670 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001671 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001672 # Issue is in the commit queue.
1673 return 'commit'
1674
1675 try:
1676 reviewers = self.GetApprovingReviewers()
1677 except urllib2.HTTPError:
1678 return 'error'
1679
1680 if reviewers:
1681 # Was LGTM'ed.
1682 return 'lgtm'
1683
1684 messages = props.get('messages') or []
1685
1686 if not messages:
1687 # No message was sent.
1688 return 'unsent'
1689 if messages[-1]['sender'] != props.get('owner_email'):
1690 # Non-LGTM reply from non-owner
1691 return 'reply'
1692 return 'waiting'
1693
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001694 def UpdateDescriptionRemote(self, description):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001695 return self.RpcServer().update_description(
1696 self.GetIssue(), self.description)
1697
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001698 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001699 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001700
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001701 def SetFlag(self, flag, value):
1702 """Patchset must match."""
1703 if not self.GetPatchset():
1704 DieWithError('The patchset needs to match. Send another patchset.')
1705 try:
1706 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001707 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001708 except urllib2.HTTPError, e:
1709 if e.code == 404:
1710 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1711 if e.code == 403:
1712 DieWithError(
1713 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1714 'match?') % (self.GetIssue(), self.GetPatchset()))
1715 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001716
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001717 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001718 """Returns an upload.RpcServer() to access this review's rietveld instance.
1719 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001720 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001721 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001722 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001723 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001724 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001725
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001726 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001727 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001728 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001729
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001730 def PatchsetSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001731 """Return the git setting that stores this change's most recent patchset."""
1732 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1733
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001734 def GetCodereviewServerSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001735 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001736 branch = self.GetBranch()
1737 if branch:
1738 return 'branch.%s.rietveldserver' % branch
1739 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001740
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001741 def GetRieveldObjForPresubmit(self):
1742 return self.RpcServer()
1743
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001744 def SetCQState(self, new_state):
1745 props = self.GetIssueProperties()
1746 if props.get('private'):
1747 DieWithError('Cannot set-commit on private issue')
1748
1749 if new_state == _CQState.COMMIT:
1750 self.SetFlag('commit', '1')
1751 elif new_state == _CQState.NONE:
1752 self.SetFlag('commit', '0')
1753 else:
1754 raise NotImplementedError()
1755
1756
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001757 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1758 directory):
1759 # TODO(maruel): Use apply_issue.py
1760
1761 # PatchIssue should never be called with a dirty tree. It is up to the
1762 # caller to check this, but just in case we assert here since the
1763 # consequences of the caller not checking this could be dire.
1764 assert(not git_common.is_dirty_git_tree('apply'))
1765 assert(parsed_issue_arg.valid)
1766 self._changelist.issue = parsed_issue_arg.issue
1767 if parsed_issue_arg.hostname:
1768 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1769
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001770 if (isinstance(parsed_issue_arg, _RietveldParsedIssueNumberArgument) and
1771 parsed_issue_arg.patch_url):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001772 assert parsed_issue_arg.patchset
1773 patchset = parsed_issue_arg.patchset
1774 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1775 else:
1776 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1777 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1778
1779 # Switch up to the top-level directory, if necessary, in preparation for
1780 # applying the patch.
1781 top = settings.GetRelativeRoot()
1782 if top:
1783 os.chdir(top)
1784
1785 # Git patches have a/ at the beginning of source paths. We strip that out
1786 # with a sed script rather than the -p flag to patch so we can feed either
1787 # Git or svn-style patches into the same apply command.
1788 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1789 try:
1790 patch_data = subprocess2.check_output(
1791 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1792 except subprocess2.CalledProcessError:
1793 DieWithError('Git patch mungling failed.')
1794 logging.info(patch_data)
1795
1796 # We use "git apply" to apply the patch instead of "patch" so that we can
1797 # pick up file adds.
1798 # The --index flag means: also insert into the index (so we catch adds).
1799 cmd = ['git', 'apply', '--index', '-p0']
1800 if directory:
1801 cmd.extend(('--directory', directory))
1802 if reject:
1803 cmd.append('--reject')
1804 elif IsGitVersionAtLeast('1.7.12'):
1805 cmd.append('--3way')
1806 try:
1807 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
1808 stdin=patch_data, stdout=subprocess2.VOID)
1809 except subprocess2.CalledProcessError:
1810 print 'Failed to apply the patch'
1811 return 1
1812
1813 # If we had an issue, commit the current state and register the issue.
1814 if not nocommit:
1815 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
1816 'patch from issue %(i)s at patchset '
1817 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
1818 % {'i': self.GetIssue(), 'p': patchset})])
1819 self.SetIssue(self.GetIssue())
1820 self.SetPatchset(patchset)
1821 print "Committed patch locally."
1822 else:
1823 print "Patch applied to index."
1824 return 0
1825
1826 @staticmethod
1827 def ParseIssueURL(parsed_url):
1828 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1829 return None
1830 # Typical url: https://domain/<issue_number>[/[other]]
1831 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
1832 if match:
1833 return _RietveldParsedIssueNumberArgument(
1834 issue=int(match.group(1)),
1835 hostname=parsed_url.netloc)
1836 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
1837 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
1838 if match:
1839 return _RietveldParsedIssueNumberArgument(
1840 issue=int(match.group(1)),
1841 patchset=int(match.group(2)),
1842 hostname=parsed_url.netloc,
1843 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
1844 return None
1845
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001846 def CMDUploadChange(self, options, args, change):
1847 """Upload the patch to Rietveld."""
1848 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1849 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001850 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
1851 if options.emulate_svn_auto_props:
1852 upload_args.append('--emulate_svn_auto_props')
1853
1854 change_desc = None
1855
1856 if options.email is not None:
1857 upload_args.extend(['--email', options.email])
1858
1859 if self.GetIssue():
1860 if options.title:
1861 upload_args.extend(['--title', options.title])
1862 if options.message:
1863 upload_args.extend(['--message', options.message])
1864 upload_args.extend(['--issue', str(self.GetIssue())])
1865 print ('This branch is associated with issue %s. '
1866 'Adding patch to that issue.' % self.GetIssue())
1867 else:
1868 if options.title:
1869 upload_args.extend(['--title', options.title])
1870 message = (options.title or options.message or
1871 CreateDescriptionFromLog(args))
1872 change_desc = ChangeDescription(message)
1873 if options.reviewers or options.tbr_owners:
1874 change_desc.update_reviewers(options.reviewers,
1875 options.tbr_owners,
1876 change)
1877 if not options.force:
1878 change_desc.prompt()
1879
1880 if not change_desc.description:
1881 print "Description is empty; aborting."
1882 return 1
1883
1884 upload_args.extend(['--message', change_desc.description])
1885 if change_desc.get_reviewers():
1886 upload_args.append('--reviewers=%s' % ','.join(
1887 change_desc.get_reviewers()))
1888 if options.send_mail:
1889 if not change_desc.get_reviewers():
1890 DieWithError("Must specify reviewers to send email.")
1891 upload_args.append('--send_mail')
1892
1893 # We check this before applying rietveld.private assuming that in
1894 # rietveld.cc only addresses which we can send private CLs to are listed
1895 # if rietveld.private is set, and so we should ignore rietveld.cc only
1896 # when --private is specified explicitly on the command line.
1897 if options.private:
1898 logging.warn('rietveld.cc is ignored since private flag is specified. '
1899 'You need to review and add them manually if necessary.')
1900 cc = self.GetCCListWithoutDefault()
1901 else:
1902 cc = self.GetCCList()
1903 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
1904 if cc:
1905 upload_args.extend(['--cc', cc])
1906
1907 if options.private or settings.GetDefaultPrivateFlag() == "True":
1908 upload_args.append('--private')
1909
1910 upload_args.extend(['--git_similarity', str(options.similarity)])
1911 if not options.find_copies:
1912 upload_args.extend(['--git_no_find_copies'])
1913
1914 # Include the upstream repo's URL in the change -- this is useful for
1915 # projects that have their source spread across multiple repos.
1916 remote_url = self.GetGitBaseUrlFromConfig()
1917 if not remote_url:
1918 if settings.GetIsGitSvn():
1919 remote_url = self.GetGitSvnRemoteUrl()
1920 else:
1921 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
1922 remote_url = '%s@%s' % (self.GetRemoteUrl(),
1923 self.GetUpstreamBranch().split('/')[-1])
1924 if remote_url:
1925 upload_args.extend(['--base_url', remote_url])
1926 remote, remote_branch = self.GetRemoteBranch()
1927 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
1928 settings.GetPendingRefPrefix())
1929 if target_ref:
1930 upload_args.extend(['--target_ref', target_ref])
1931
1932 # Look for dependent patchsets. See crbug.com/480453 for more details.
1933 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
1934 upstream_branch = ShortBranchName(upstream_branch)
1935 if remote is '.':
1936 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001937 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001938 if settings.GetIsSkipDependencyUpload(local_branch):
1939 print
1940 print ('Skipping dependency patchset upload because git config '
1941 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
1942 print
1943 else:
1944 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001945 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001946 auth_config=auth_config)
1947 branch_cl_issue_url = branch_cl.GetIssueURL()
1948 branch_cl_issue = branch_cl.GetIssue()
1949 branch_cl_patchset = branch_cl.GetPatchset()
1950 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
1951 upload_args.extend(
1952 ['--depends_on_patchset', '%s:%s' % (
1953 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001954 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001955 '\n'
1956 'The current branch (%s) is tracking a local branch (%s) with '
1957 'an associated CL.\n'
1958 'Adding %s/#ps%s as a dependency patchset.\n'
1959 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
1960 branch_cl_patchset))
1961
1962 project = settings.GetProject()
1963 if project:
1964 upload_args.extend(['--project', project])
1965
1966 if options.cq_dry_run:
1967 upload_args.extend(['--cq_dry_run'])
1968
1969 try:
1970 upload_args = ['upload'] + upload_args + args
1971 logging.info('upload.RealMain(%s)', upload_args)
1972 issue, patchset = upload.RealMain(upload_args)
1973 issue = int(issue)
1974 patchset = int(patchset)
1975 except KeyboardInterrupt:
1976 sys.exit(1)
1977 except:
1978 # If we got an exception after the user typed a description for their
1979 # change, back up the description before re-raising.
1980 if change_desc:
1981 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1982 print('\nGot exception while uploading -- saving description to %s\n' %
1983 backup_path)
1984 backup_file = open(backup_path, 'w')
1985 backup_file.write(change_desc.description)
1986 backup_file.close()
1987 raise
1988
1989 if not self.GetIssue():
1990 self.SetIssue(issue)
1991 self.SetPatchset(patchset)
1992
1993 if options.use_commit_queue:
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001994 self.SetCQState(_CQState.COMMIT)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001995 return 0
1996
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001997
1998class _GerritChangelistImpl(_ChangelistCodereviewBase):
1999 def __init__(self, changelist, auth_config=None):
2000 # auth_config is Rietveld thing, kept here to preserve interface only.
2001 super(_GerritChangelistImpl, self).__init__(changelist)
2002 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002003 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002004 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002005 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002006
2007 def _GetGerritHost(self):
2008 # Lazy load of configs.
2009 self.GetCodereviewServer()
2010 return self._gerrit_host
2011
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002012 def _GetGitHost(self):
2013 """Returns git host to be used when uploading change to Gerrit."""
2014 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2015
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002016 def GetCodereviewServer(self):
2017 if not self._gerrit_server:
2018 # If we're on a branch then get the server potentially associated
2019 # with that branch.
2020 if self.GetIssue():
2021 gerrit_server_setting = self.GetCodereviewServerSetting()
2022 if gerrit_server_setting:
2023 self._gerrit_server = RunGit(['config', gerrit_server_setting],
2024 error_ok=True).strip()
2025 if self._gerrit_server:
2026 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
2027 if not self._gerrit_server:
2028 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2029 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002030 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002031 parts[0] = parts[0] + '-review'
2032 self._gerrit_host = '.'.join(parts)
2033 self._gerrit_server = 'https://%s' % self._gerrit_host
2034 return self._gerrit_server
2035
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002036 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00002037 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002038 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002039
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002040 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002041 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002042 if settings.GetGerritSkipEnsureAuthenticated():
2043 # For projects with unusual authentication schemes.
2044 # See http://crbug.com/603378.
2045 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002046 # Lazy-loader to identify Gerrit and Git hosts.
2047 if gerrit_util.GceAuthenticator.is_gce():
2048 return
2049 self.GetCodereviewServer()
2050 git_host = self._GetGitHost()
2051 assert self._gerrit_server and self._gerrit_host
2052 cookie_auth = gerrit_util.CookiesAuthenticator()
2053
2054 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2055 git_auth = cookie_auth.get_auth_header(git_host)
2056 if gerrit_auth and git_auth:
2057 if gerrit_auth == git_auth:
2058 return
2059 print((
2060 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2061 ' Check your %s or %s file for credentials of hosts:\n'
2062 ' %s\n'
2063 ' %s\n'
2064 ' %s') %
2065 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2066 git_host, self._gerrit_host,
2067 cookie_auth.get_new_password_message(git_host)))
2068 if not force:
2069 ask_for_data('If you know what you are doing, press Enter to continue, '
2070 'Ctrl+C to abort.')
2071 return
2072 else:
2073 missing = (
2074 [] if gerrit_auth else [self._gerrit_host] +
2075 [] if git_auth else [git_host])
2076 DieWithError('Credentials for the following hosts are required:\n'
2077 ' %s\n'
2078 'These are read from %s (or legacy %s)\n'
2079 '%s' % (
2080 '\n '.join(missing),
2081 cookie_auth.get_gitcookies_path(),
2082 cookie_auth.get_netrc_path(),
2083 cookie_auth.get_new_password_message(git_host)))
2084
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002085
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002086 def PatchsetSetting(self):
2087 """Return the git setting that stores this change's most recent patchset."""
2088 return 'branch.%s.gerritpatchset' % self.GetBranch()
2089
2090 def GetCodereviewServerSetting(self):
2091 """Returns the git setting that stores this change's Gerrit server."""
2092 branch = self.GetBranch()
2093 if branch:
2094 return 'branch.%s.gerritserver' % branch
2095 return None
2096
2097 def GetRieveldObjForPresubmit(self):
2098 class ThisIsNotRietveldIssue(object):
2099 def __nonzero__(self):
2100 # This is a hack to make presubmit_support think that rietveld is not
2101 # defined, yet still ensure that calls directly result in a decent
2102 # exception message below.
2103 return False
2104
2105 def __getattr__(self, attr):
2106 print(
2107 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2108 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2109 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2110 'or use Rietveld for codereview.\n'
2111 'See also http://crbug.com/579160.' % attr)
2112 raise NotImplementedError()
2113 return ThisIsNotRietveldIssue()
2114
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002115 def GetGerritObjForPresubmit(self):
2116 return presubmit_support.GerritAccessor(self._GetGerritHost())
2117
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002118 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002119 """Apply a rough heuristic to give a simple summary of an issue's review
2120 or CQ status, assuming adherence to a common workflow.
2121
2122 Returns None if no issue for this branch, or one of the following keywords:
2123 * 'error' - error from review tool (including deleted issues)
2124 * 'unsent' - no reviewers added
2125 * 'waiting' - waiting for review
2126 * 'reply' - waiting for owner to reply to review
2127 * 'not lgtm' - Code-Review -2 from at least one approved reviewer
2128 * 'lgtm' - Code-Review +2 from at least one approved reviewer
2129 * 'commit' - in the commit queue
2130 * 'closed' - abandoned
2131 """
2132 if not self.GetIssue():
2133 return None
2134
2135 try:
2136 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
2137 except httplib.HTTPException:
2138 return 'error'
2139
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002140 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002141 return 'closed'
2142
2143 cq_label = data['labels'].get('Commit-Queue', {})
2144 if cq_label:
2145 # Vote value is a stringified integer, which we expect from 0 to 2.
2146 vote_value = cq_label.get('value', '0')
2147 vote_text = cq_label.get('values', {}).get(vote_value, '')
2148 if vote_text.lower() == 'commit':
2149 return 'commit'
2150
2151 lgtm_label = data['labels'].get('Code-Review', {})
2152 if lgtm_label:
2153 if 'rejected' in lgtm_label:
2154 return 'not lgtm'
2155 if 'approved' in lgtm_label:
2156 return 'lgtm'
2157
2158 if not data.get('reviewers', {}).get('REVIEWER', []):
2159 return 'unsent'
2160
2161 messages = data.get('messages', [])
2162 if messages:
2163 owner = data['owner'].get('_account_id')
2164 last_message_author = messages[-1].get('author', {}).get('_account_id')
2165 if owner != last_message_author:
2166 # Some reply from non-owner.
2167 return 'reply'
2168
2169 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002170
2171 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002172 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002173 return data['revisions'][data['current_revision']]['_number']
2174
2175 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002176 data = self._GetChangeDetail(['CURRENT_REVISION'])
2177 current_rev = data['current_revision']
2178 url = data['revisions'][current_rev]['fetch']['http']['url']
2179 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002180
2181 def UpdateDescriptionRemote(self, description):
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002182 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
2183 description)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002184
2185 def CloseIssue(self):
2186 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2187
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002188 def GetApprovingReviewers(self):
2189 """Returns a list of reviewers approving the change.
2190
2191 Note: not necessarily committers.
2192 """
2193 raise NotImplementedError()
2194
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002195 def SubmitIssue(self, wait_for_merge=True):
2196 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2197 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002198
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002199 def _GetChangeDetail(self, options=None, issue=None):
2200 options = options or []
2201 issue = issue or self.GetIssue()
2202 assert issue, 'issue required to query Gerrit'
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002203 return gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2204 options)
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002205
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002206 def CMDLand(self, force, bypass_hooks, verbose):
2207 if git_common.is_dirty_git_tree('land'):
2208 return 1
2209 differs = True
2210 last_upload = RunGit(['config',
2211 'branch.%s.gerritsquashhash' % self.GetBranch()],
2212 error_ok=True).strip()
2213 # Note: git diff outputs nothing if there is no diff.
2214 if not last_upload or RunGit(['diff', last_upload]).strip():
2215 print('WARNING: some changes from local branch haven\'t been uploaded')
2216 else:
2217 detail = self._GetChangeDetail(['CURRENT_REVISION'])
2218 if detail['current_revision'] == last_upload:
2219 differs = False
2220 else:
2221 print('WARNING: local branch contents differ from latest uploaded '
2222 'patchset')
2223 if differs:
2224 if not force:
2225 ask_for_data(
2226 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2227 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2228 elif not bypass_hooks:
2229 hook_results = self.RunHook(
2230 committing=True,
2231 may_prompt=not force,
2232 verbose=verbose,
2233 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2234 if not hook_results.should_continue():
2235 return 1
2236
2237 self.SubmitIssue(wait_for_merge=True)
2238 print('Issue %s has been submitted.' % self.GetIssueURL())
2239 return 0
2240
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002241 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2242 directory):
2243 assert not reject
2244 assert not nocommit
2245 assert not directory
2246 assert parsed_issue_arg.valid
2247
2248 self._changelist.issue = parsed_issue_arg.issue
2249
2250 if parsed_issue_arg.hostname:
2251 self._gerrit_host = parsed_issue_arg.hostname
2252 self._gerrit_server = 'https://%s' % self._gerrit_host
2253
2254 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2255
2256 if not parsed_issue_arg.patchset:
2257 # Use current revision by default.
2258 revision_info = detail['revisions'][detail['current_revision']]
2259 patchset = int(revision_info['_number'])
2260 else:
2261 patchset = parsed_issue_arg.patchset
2262 for revision_info in detail['revisions'].itervalues():
2263 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2264 break
2265 else:
2266 DieWithError('Couldn\'t find patchset %i in issue %i' %
2267 (parsed_issue_arg.patchset, self.GetIssue()))
2268
2269 fetch_info = revision_info['fetch']['http']
2270 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2271 RunGit(['cherry-pick', 'FETCH_HEAD'])
2272 self.SetIssue(self.GetIssue())
2273 self.SetPatchset(patchset)
2274 print('Committed patch for issue %i pathset %i locally' %
2275 (self.GetIssue(), self.GetPatchset()))
2276 return 0
2277
2278 @staticmethod
2279 def ParseIssueURL(parsed_url):
2280 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2281 return None
2282 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2283 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2284 # Short urls like https://domain/<issue_number> can be used, but don't allow
2285 # specifying the patchset (you'd 404), but we allow that here.
2286 if parsed_url.path == '/':
2287 part = parsed_url.fragment
2288 else:
2289 part = parsed_url.path
2290 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2291 if match:
2292 return _ParsedIssueNumberArgument(
2293 issue=int(match.group(2)),
2294 patchset=int(match.group(4)) if match.group(4) else None,
2295 hostname=parsed_url.netloc)
2296 return None
2297
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002298 def CMDUploadChange(self, options, args, change):
2299 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002300 if options.squash and options.no_squash:
2301 DieWithError('Can only use one of --squash or --no-squash')
2302 options.squash = ((settings.GetSquashGerritUploads() or options.squash) and
2303 not options.no_squash)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002304 # We assume the remote called "origin" is the one we want.
2305 # It is probably not worthwhile to support different workflows.
2306 gerrit_remote = 'origin'
2307
2308 remote, remote_branch = self.GetRemoteBranch()
2309 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2310 pending_prefix='')
2311
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002312 if options.squash:
2313 if not self.GetIssue():
2314 # TODO(tandrii): deperecate this after 2016Q2. Backwards compatibility
2315 # with shadow branch, which used to contain change-id for a given
2316 # branch, using which we can fetch actual issue number and set it as the
2317 # property of the branch, which is the new way.
2318 message = RunGitSilent([
2319 'show', '--format=%B', '-s',
2320 'refs/heads/git_cl_uploads/%s' % self.GetBranch()])
2321 if message:
2322 change_ids = git_footers.get_footer_change_id(message.strip())
2323 if change_ids and len(change_ids) == 1:
2324 details = self._GetChangeDetail(issue=change_ids[0])
2325 if details:
2326 print('WARNING: found old upload in branch git_cl_uploads/%s '
2327 'corresponding to issue %s' %
2328 (self.GetBranch(), details['_number']))
2329 self.SetIssue(details['_number'])
2330 if not self.GetIssue():
2331 DieWithError(
2332 '\n' # For readability of the blob below.
2333 'Found old upload in branch git_cl_uploads/%s, '
2334 'but failed to find corresponding Gerrit issue.\n'
2335 'If you know the issue number, set it manually first:\n'
2336 ' git cl issue 123456\n'
2337 'If you intended to upload this CL as new issue, '
2338 'just delete or rename the old upload branch:\n'
2339 ' git rename-branch git_cl_uploads/%s old_upload-%s\n'
2340 'After that, please run git cl upload again.' %
2341 tuple([self.GetBranch()] * 3))
2342 # End of backwards compatability.
2343
2344 if self.GetIssue():
2345 # Try to get the message from a previous upload.
2346 message = self.GetDescription()
2347 if not message:
2348 DieWithError(
2349 'failed to fetch description from current Gerrit issue %d\n'
2350 '%s' % (self.GetIssue(), self.GetIssueURL()))
2351 change_id = self._GetChangeDetail()['change_id']
2352 while True:
2353 footer_change_ids = git_footers.get_footer_change_id(message)
2354 if footer_change_ids == [change_id]:
2355 break
2356 if not footer_change_ids:
2357 message = git_footers.add_footer_change_id(message, change_id)
2358 print('WARNING: appended missing Change-Id to issue description')
2359 continue
2360 # There is already a valid footer but with different or several ids.
2361 # Doing this automatically is non-trivial as we don't want to lose
2362 # existing other footers, yet we want to append just 1 desired
2363 # Change-Id. Thus, just create a new footer, but let user verify the
2364 # new description.
2365 message = '%s\n\nChange-Id: %s' % (message, change_id)
2366 print(
2367 'WARNING: issue %s has Change-Id footer(s):\n'
2368 ' %s\n'
2369 'but issue has Change-Id %s, according to Gerrit.\n'
2370 'Please, check the proposed correction to the description, '
2371 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2372 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2373 change_id))
2374 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2375 if not options.force:
2376 change_desc = ChangeDescription(message)
2377 change_desc.prompt()
2378 message = change_desc.description
2379 if not message:
2380 DieWithError("Description is empty. Aborting...")
2381 # Continue the while loop.
2382 # Sanity check of this code - we should end up with proper message
2383 # footer.
2384 assert [change_id] == git_footers.get_footer_change_id(message)
2385 change_desc = ChangeDescription(message)
2386 else:
2387 change_desc = ChangeDescription(
2388 options.message or CreateDescriptionFromLog(args))
2389 if not options.force:
2390 change_desc.prompt()
2391 if not change_desc.description:
2392 DieWithError("Description is empty. Aborting...")
2393 message = change_desc.description
2394 change_ids = git_footers.get_footer_change_id(message)
2395 if len(change_ids) > 1:
2396 DieWithError('too many Change-Id footers, at most 1 allowed.')
2397 if not change_ids:
2398 # Generate the Change-Id automatically.
2399 message = git_footers.add_footer_change_id(
2400 message, GenerateGerritChangeId(message))
2401 change_desc.set_description(message)
2402 change_ids = git_footers.get_footer_change_id(message)
2403 assert len(change_ids) == 1
2404 change_id = change_ids[0]
2405
2406 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2407 if remote is '.':
2408 # If our upstream branch is local, we base our squashed commit on its
2409 # squashed version.
2410 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2411 # Check the squashed hash of the parent.
2412 parent = RunGit(['config',
2413 'branch.%s.gerritsquashhash' % upstream_branch_name],
2414 error_ok=True).strip()
2415 # Verify that the upstream branch has been uploaded too, otherwise
2416 # Gerrit will create additional CLs when uploading.
2417 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2418 RunGitSilent(['rev-parse', parent + ':'])):
2419 # TODO(tandrii): remove "old depot_tools" part on April 12, 2016.
2420 DieWithError(
2421 'Upload upstream branch %s first.\n'
2422 'Note: maybe you\'ve uploaded it with --no-squash or with an old '
2423 'version of depot_tools. If so, then re-upload it with:\n'
2424 ' git cl upload --squash\n' % upstream_branch_name)
2425 else:
2426 parent = self.GetCommonAncestorWithUpstream()
2427
2428 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2429 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2430 '-m', message]).strip()
2431 else:
2432 change_desc = ChangeDescription(
2433 options.message or CreateDescriptionFromLog(args))
2434 if not change_desc.description:
2435 DieWithError("Description is empty. Aborting...")
2436
2437 if not git_footers.get_footer_change_id(change_desc.description):
2438 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002439 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2440 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002441 ref_to_push = 'HEAD'
2442 parent = '%s/%s' % (gerrit_remote, branch)
2443 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2444
2445 assert change_desc
2446 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2447 ref_to_push)]).splitlines()
2448 if len(commits) > 1:
2449 print('WARNING: This will upload %d commits. Run the following command '
2450 'to see which commits will be uploaded: ' % len(commits))
2451 print('git log %s..%s' % (parent, ref_to_push))
2452 print('You can also use `git squash-branch` to squash these into a '
2453 'single commit.')
2454 ask_for_data('About to upload; enter to confirm.')
2455
2456 if options.reviewers or options.tbr_owners:
2457 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2458 change)
2459
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002460 # Extra options that can be specified at push time. Doc:
2461 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2462 refspec_opts = []
2463 if options.title:
2464 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2465 # reverse on its side.
2466 if '_' in options.title:
2467 print('WARNING: underscores in title will be converted to spaces.')
2468 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2469
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002470 if options.send_mail:
2471 if not change_desc.get_reviewers():
2472 DieWithError('Must specify reviewers to send email.')
2473 refspec_opts.append('notify=ALL')
2474 else:
2475 refspec_opts.append('notify=NONE')
2476
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002477 cc = self.GetCCList().split(',')
2478 if options.cc:
2479 cc.extend(options.cc)
2480 cc = filter(None, cc)
2481 if cc:
tandrii@chromium.org0b2d7072016-04-18 16:19:03 +00002482 # refspec_opts.extend('cc=' + email.strip() for email in cc)
2483 # TODO(tandrii): enable this back. http://crbug.com/604377
2484 print('WARNING: Gerrit doesn\'t yet support cc-ing arbitrary emails.\n'
2485 ' Ignoring cc-ed emails. See http://crbug.com/604377.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002486
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002487 if change_desc.get_reviewers():
2488 refspec_opts.extend('r=' + email.strip()
2489 for email in change_desc.get_reviewers())
2490
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002491 refspec_suffix = ''
2492 if refspec_opts:
2493 refspec_suffix = '%' + ','.join(refspec_opts)
2494 assert ' ' not in refspec_suffix, (
2495 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002496 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002497
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002498 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002499 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002500 print_stdout=True,
2501 # Flush after every line: useful for seeing progress when running as
2502 # recipe.
2503 filter_fn=lambda _: sys.stdout.flush())
2504
2505 if options.squash:
2506 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2507 change_numbers = [m.group(1)
2508 for m in map(regex.match, push_stdout.splitlines())
2509 if m]
2510 if len(change_numbers) != 1:
2511 DieWithError(
2512 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2513 'Change-Id: %s') % (len(change_numbers), change_id))
2514 self.SetIssue(change_numbers[0])
2515 RunGit(['config', 'branch.%s.gerritsquashhash' % self.GetBranch(),
2516 ref_to_push])
2517 return 0
2518
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002519 def _AddChangeIdToCommitMessage(self, options, args):
2520 """Re-commits using the current message, assumes the commit hook is in
2521 place.
2522 """
2523 log_desc = options.message or CreateDescriptionFromLog(args)
2524 git_command = ['commit', '--amend', '-m', log_desc]
2525 RunGit(git_command)
2526 new_log_desc = CreateDescriptionFromLog(args)
2527 if git_footers.get_footer_change_id(new_log_desc):
2528 print 'git-cl: Added Change-Id to commit message.'
2529 return new_log_desc
2530 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002531 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002532
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002533 def SetCQState(self, new_state):
2534 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
2535 # TODO(tandrii): maybe allow configurability in codereview.settings or by
2536 # self-discovery of label config for this CL using REST API.
2537 vote_map = {
2538 _CQState.NONE: 0,
2539 _CQState.DRY_RUN: 1,
2540 _CQState.COMMIT : 2,
2541 }
2542 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2543 labels={'Commit-Queue': vote_map[new_state]})
2544
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002545
2546_CODEREVIEW_IMPLEMENTATIONS = {
2547 'rietveld': _RietveldChangelistImpl,
2548 'gerrit': _GerritChangelistImpl,
2549}
2550
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002551
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002552def _add_codereview_select_options(parser):
2553 """Appends --gerrit and --rietveld options to force specific codereview."""
2554 parser.codereview_group = optparse.OptionGroup(
2555 parser, 'EXPERIMENTAL! Codereview override options')
2556 parser.add_option_group(parser.codereview_group)
2557 parser.codereview_group.add_option(
2558 '--gerrit', action='store_true',
2559 help='Force the use of Gerrit for codereview')
2560 parser.codereview_group.add_option(
2561 '--rietveld', action='store_true',
2562 help='Force the use of Rietveld for codereview')
2563
2564
2565def _process_codereview_select_options(parser, options):
2566 if options.gerrit and options.rietveld:
2567 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2568 options.forced_codereview = None
2569 if options.gerrit:
2570 options.forced_codereview = 'gerrit'
2571 elif options.rietveld:
2572 options.forced_codereview = 'rietveld'
2573
2574
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002575class ChangeDescription(object):
2576 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002577 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002578 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002579
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002580 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002581 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002582
agable@chromium.org42c20792013-09-12 17:34:49 +00002583 @property # www.logilab.org/ticket/89786
2584 def description(self): # pylint: disable=E0202
2585 return '\n'.join(self._description_lines)
2586
2587 def set_description(self, desc):
2588 if isinstance(desc, basestring):
2589 lines = desc.splitlines()
2590 else:
2591 lines = [line.rstrip() for line in desc]
2592 while lines and not lines[0]:
2593 lines.pop(0)
2594 while lines and not lines[-1]:
2595 lines.pop(-1)
2596 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002597
piman@chromium.org336f9122014-09-04 02:16:55 +00002598 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002599 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002600 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002601 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002602 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002603 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002604
agable@chromium.org42c20792013-09-12 17:34:49 +00002605 # Get the set of R= and TBR= lines and remove them from the desciption.
2606 regexp = re.compile(self.R_LINE)
2607 matches = [regexp.match(line) for line in self._description_lines]
2608 new_desc = [l for i, l in enumerate(self._description_lines)
2609 if not matches[i]]
2610 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002611
agable@chromium.org42c20792013-09-12 17:34:49 +00002612 # Construct new unified R= and TBR= lines.
2613 r_names = []
2614 tbr_names = []
2615 for match in matches:
2616 if not match:
2617 continue
2618 people = cleanup_list([match.group(2).strip()])
2619 if match.group(1) == 'TBR':
2620 tbr_names.extend(people)
2621 else:
2622 r_names.extend(people)
2623 for name in r_names:
2624 if name not in reviewers:
2625 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002626 if add_owners_tbr:
2627 owners_db = owners.Database(change.RepositoryRoot(),
2628 fopen=file, os_path=os.path, glob=glob.glob)
2629 all_reviewers = set(tbr_names + reviewers)
2630 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2631 all_reviewers)
2632 tbr_names.extend(owners_db.reviewers_for(missing_files,
2633 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002634 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2635 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2636
2637 # Put the new lines in the description where the old first R= line was.
2638 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2639 if 0 <= line_loc < len(self._description_lines):
2640 if new_tbr_line:
2641 self._description_lines.insert(line_loc, new_tbr_line)
2642 if new_r_line:
2643 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002644 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002645 if new_r_line:
2646 self.append_footer(new_r_line)
2647 if new_tbr_line:
2648 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002649
2650 def prompt(self):
2651 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002652 self.set_description([
2653 '# Enter a description of the change.',
2654 '# This will be displayed on the codereview site.',
2655 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002656 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002657 '--------------------',
2658 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002659
agable@chromium.org42c20792013-09-12 17:34:49 +00002660 regexp = re.compile(self.BUG_LINE)
2661 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00002662 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00002663 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002664 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002665 if not content:
2666 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002667 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002668
2669 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002670 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2671 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002672 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002673 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002674
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002675 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00002676 if self._description_lines:
2677 # Add an empty line if either the last line or the new line isn't a tag.
2678 last_line = self._description_lines[-1]
2679 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
2680 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2681 self._description_lines.append('')
2682 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002683
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002684 def get_reviewers(self):
2685 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002686 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
2687 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002688 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002689
2690
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002691def get_approving_reviewers(props):
2692 """Retrieves the reviewers that approved a CL from the issue properties with
2693 messages.
2694
2695 Note that the list may contain reviewers that are not committer, thus are not
2696 considered by the CQ.
2697 """
2698 return sorted(
2699 set(
2700 message['sender']
2701 for message in props['messages']
2702 if message['approval'] and message['sender'] in props['reviewers']
2703 )
2704 )
2705
2706
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002707def FindCodereviewSettingsFile(filename='codereview.settings'):
2708 """Finds the given file starting in the cwd and going up.
2709
2710 Only looks up to the top of the repository unless an
2711 'inherit-review-settings-ok' file exists in the root of the repository.
2712 """
2713 inherit_ok_file = 'inherit-review-settings-ok'
2714 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002715 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002716 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2717 root = '/'
2718 while True:
2719 if filename in os.listdir(cwd):
2720 if os.path.isfile(os.path.join(cwd, filename)):
2721 return open(os.path.join(cwd, filename))
2722 if cwd == root:
2723 break
2724 cwd = os.path.dirname(cwd)
2725
2726
2727def LoadCodereviewSettingsFromFile(fileobj):
2728 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002729 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002730
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002731 def SetProperty(name, setting, unset_error_ok=False):
2732 fullname = 'rietveld.' + name
2733 if setting in keyvals:
2734 RunGit(['config', fullname, keyvals[setting]])
2735 else:
2736 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2737
2738 SetProperty('server', 'CODE_REVIEW_SERVER')
2739 # Only server setting is required. Other settings can be absent.
2740 # In that case, we ignore errors raised during option deletion attempt.
2741 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002742 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002743 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2744 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002745 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002746 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002747 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2748 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002749 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002750 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002751 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002752 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2753 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002754
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002755 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002756 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002757
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002758 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
2759 RunGit(['config', 'gerrit.squash-uploads',
2760 keyvals['GERRIT_SQUASH_UPLOADS']])
2761
tandrii@chromium.org28253532016-04-14 13:46:56 +00002762 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00002763 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00002764 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
2765
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002766 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2767 #should be of the form
2768 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2769 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2770 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2771 keyvals['ORIGIN_URL_CONFIG']])
2772
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002773
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002774def urlretrieve(source, destination):
2775 """urllib is broken for SSL connections via a proxy therefore we
2776 can't use urllib.urlretrieve()."""
2777 with open(destination, 'w') as f:
2778 f.write(urllib2.urlopen(source).read())
2779
2780
ukai@chromium.org712d6102013-11-27 00:52:58 +00002781def hasSheBang(fname):
2782 """Checks fname is a #! script."""
2783 with open(fname) as f:
2784 return f.read(2).startswith('#!')
2785
2786
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00002787# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
2788def DownloadHooks(*args, **kwargs):
2789 pass
2790
2791
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002792def DownloadGerritHook(force):
2793 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002794
2795 Args:
2796 force: True to update hooks. False to install hooks if not present.
2797 """
2798 if not settings.GetIsGerrit():
2799 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00002800 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002801 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2802 if not os.access(dst, os.X_OK):
2803 if os.path.exists(dst):
2804 if not force:
2805 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002806 try:
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002807 print(
2808 'WARNING: installing Gerrit commit-msg hook.\n'
2809 ' This behavior of git cl will soon be disabled.\n'
2810 ' See bug http://crbug.com/579176.')
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002811 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002812 if not hasSheBang(dst):
2813 DieWithError('Not a script: %s\n'
2814 'You need to download from\n%s\n'
2815 'into .git/hooks/commit-msg and '
2816 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002817 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2818 except Exception:
2819 if os.path.exists(dst):
2820 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002821 DieWithError('\nFailed to download hooks.\n'
2822 'You need to download from\n%s\n'
2823 'into .git/hooks/commit-msg and '
2824 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002825
2826
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002827
2828def GetRietveldCodereviewSettingsInteractively():
2829 """Prompt the user for settings."""
2830 server = settings.GetDefaultServerUrl(error_ok=True)
2831 prompt = 'Rietveld server (host[:port])'
2832 prompt += ' [%s]' % (server or DEFAULT_SERVER)
2833 newserver = ask_for_data(prompt + ':')
2834 if not server and not newserver:
2835 newserver = DEFAULT_SERVER
2836 if newserver:
2837 newserver = gclient_utils.UpgradeToHttps(newserver)
2838 if newserver != server:
2839 RunGit(['config', 'rietveld.server', newserver])
2840
2841 def SetProperty(initial, caption, name, is_url):
2842 prompt = caption
2843 if initial:
2844 prompt += ' ("x" to clear) [%s]' % initial
2845 new_val = ask_for_data(prompt + ':')
2846 if new_val == 'x':
2847 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
2848 elif new_val:
2849 if is_url:
2850 new_val = gclient_utils.UpgradeToHttps(new_val)
2851 if new_val != initial:
2852 RunGit(['config', 'rietveld.' + name, new_val])
2853
2854 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
2855 SetProperty(settings.GetDefaultPrivateFlag(),
2856 'Private flag (rietveld only)', 'private', False)
2857 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
2858 'tree-status-url', False)
2859 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
2860 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
2861 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
2862 'run-post-upload-hook', False)
2863
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002864@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002865def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002866 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002867
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002868 print('WARNING: git cl config works for Rietveld only.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002869 'For Gerrit, see http://crbug.com/603116.')
2870 # TODO(tandrii): add Gerrit support as part of http://crbug.com/603116.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00002871 parser.add_option('--activate-update', action='store_true',
2872 help='activate auto-updating [rietveld] section in '
2873 '.git/config')
2874 parser.add_option('--deactivate-update', action='store_true',
2875 help='deactivate auto-updating [rietveld] section in '
2876 '.git/config')
2877 options, args = parser.parse_args(args)
2878
2879 if options.deactivate_update:
2880 RunGit(['config', 'rietveld.autoupdate', 'false'])
2881 return
2882
2883 if options.activate_update:
2884 RunGit(['config', '--unset', 'rietveld.autoupdate'])
2885 return
2886
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002887 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002888 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002889 return 0
2890
2891 url = args[0]
2892 if not url.endswith('codereview.settings'):
2893 url = os.path.join(url, 'codereview.settings')
2894
2895 # Load code review settings and download hooks (if available).
2896 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
2897 return 0
2898
2899
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002900def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002901 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002902 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
2903 branch = ShortBranchName(branchref)
2904 _, args = parser.parse_args(args)
2905 if not args:
2906 print("Current base-url:")
2907 return RunGit(['config', 'branch.%s.base-url' % branch],
2908 error_ok=False).strip()
2909 else:
2910 print("Setting base-url to %s" % args[0])
2911 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
2912 error_ok=False).strip()
2913
2914
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002915def color_for_status(status):
2916 """Maps a Changelist status to color, for CMDstatus and other tools."""
2917 return {
2918 'unsent': Fore.RED,
2919 'waiting': Fore.BLUE,
2920 'reply': Fore.YELLOW,
2921 'lgtm': Fore.GREEN,
2922 'commit': Fore.MAGENTA,
2923 'closed': Fore.CYAN,
2924 'error': Fore.WHITE,
2925 }.get(status, Fore.WHITE)
2926
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00002927
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002928def get_cl_statuses(changes, fine_grained, max_processes=None):
2929 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002930
2931 If fine_grained is true, this will fetch CL statuses from the server.
2932 Otherwise, simply indicate if there's a matching url for the given branches.
2933
2934 If max_processes is specified, it is used as the maximum number of processes
2935 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
2936 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00002937
2938 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002939 """
2940 # Silence upload.py otherwise it becomes unwieldly.
2941 upload.verbosity = 0
2942
2943 if fine_grained:
2944 # Process one branch synchronously to work through authentication, then
2945 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002946 if changes:
2947 fetch = lambda cl: (cl, cl.GetStatus())
2948 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00002949
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002950 changes_to_fetch = changes[1:]
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002951 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002952 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002953 if max_processes is not None
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002954 else len(changes_to_fetch))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00002955
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002956 fetched_cls = set()
2957 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00002958 while True:
2959 try:
2960 row = it.next(timeout=5)
2961 except multiprocessing.TimeoutError:
2962 break
2963
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002964 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00002965 yield row
2966
2967 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002968 for cl in set(changes_to_fetch) - fetched_cls:
2969 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00002970
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002971 else:
2972 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002973 for cl in changes:
2974 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002975
rmistry@google.com2dd99862015-06-22 12:22:18 +00002976
2977def upload_branch_deps(cl, args):
2978 """Uploads CLs of local branches that are dependents of the current branch.
2979
2980 If the local branch dependency tree looks like:
2981 test1 -> test2.1 -> test3.1
2982 -> test3.2
2983 -> test2.2 -> test3.3
2984
2985 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
2986 run on the dependent branches in this order:
2987 test2.1, test3.1, test3.2, test2.2, test3.3
2988
2989 Note: This function does not rebase your local dependent branches. Use it when
2990 you make a change to the parent branch that will not conflict with its
2991 dependent branches, and you would like their dependencies updated in
2992 Rietveld.
2993 """
2994 if git_common.is_dirty_git_tree('upload-branch-deps'):
2995 return 1
2996
2997 root_branch = cl.GetBranch()
2998 if root_branch is None:
2999 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3000 'Get on a branch!')
3001 if not cl.GetIssue() or not cl.GetPatchset():
3002 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3003 'patchset dependencies without an uploaded CL.')
3004
3005 branches = RunGit(['for-each-ref',
3006 '--format=%(refname:short) %(upstream:short)',
3007 'refs/heads'])
3008 if not branches:
3009 print('No local branches found.')
3010 return 0
3011
3012 # Create a dictionary of all local branches to the branches that are dependent
3013 # on it.
3014 tracked_to_dependents = collections.defaultdict(list)
3015 for b in branches.splitlines():
3016 tokens = b.split()
3017 if len(tokens) == 2:
3018 branch_name, tracked = tokens
3019 tracked_to_dependents[tracked].append(branch_name)
3020
3021 print
3022 print 'The dependent local branches of %s are:' % root_branch
3023 dependents = []
3024 def traverse_dependents_preorder(branch, padding=''):
3025 dependents_to_process = tracked_to_dependents.get(branch, [])
3026 padding += ' '
3027 for dependent in dependents_to_process:
3028 print '%s%s' % (padding, dependent)
3029 dependents.append(dependent)
3030 traverse_dependents_preorder(dependent, padding)
3031 traverse_dependents_preorder(root_branch)
3032 print
3033
3034 if not dependents:
3035 print 'There are no dependent local branches for %s' % root_branch
3036 return 0
3037
3038 print ('This command will checkout all dependent branches and run '
3039 '"git cl upload".')
3040 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3041
andybons@chromium.org962f9462016-02-03 20:00:42 +00003042 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003043 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003044 args.extend(['-t', 'Updated patchset dependency'])
3045
rmistry@google.com2dd99862015-06-22 12:22:18 +00003046 # Record all dependents that failed to upload.
3047 failures = {}
3048 # Go through all dependents, checkout the branch and upload.
3049 try:
3050 for dependent_branch in dependents:
3051 print
3052 print '--------------------------------------'
3053 print 'Running "git cl upload" from %s:' % dependent_branch
3054 RunGit(['checkout', '-q', dependent_branch])
3055 print
3056 try:
3057 if CMDupload(OptionParser(), args) != 0:
3058 print 'Upload failed for %s!' % dependent_branch
3059 failures[dependent_branch] = 1
3060 except: # pylint: disable=W0702
3061 failures[dependent_branch] = 1
3062 print
3063 finally:
3064 # Swap back to the original root branch.
3065 RunGit(['checkout', '-q', root_branch])
3066
3067 print
3068 print 'Upload complete for dependent branches!'
3069 for dependent_branch in dependents:
3070 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
3071 print ' %s : %s' % (dependent_branch, upload_status)
3072 print
3073
3074 return 0
3075
3076
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003077def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003078 """Show status of changelists.
3079
3080 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003081 - Red not sent for review or broken
3082 - Blue waiting for review
3083 - Yellow waiting for you to reply to review
3084 - Green LGTM'ed
3085 - Magenta in the commit queue
3086 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003087
3088 Also see 'git cl comments'.
3089 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003090 parser.add_option('--field',
3091 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003092 parser.add_option('-f', '--fast', action='store_true',
3093 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003094 parser.add_option(
3095 '-j', '--maxjobs', action='store', type=int,
3096 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003097
3098 auth.add_auth_options(parser)
3099 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003100 if args:
3101 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003102 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003103
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003104 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003105 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003106 if options.field.startswith('desc'):
3107 print cl.GetDescription()
3108 elif options.field == 'id':
3109 issueid = cl.GetIssue()
3110 if issueid:
3111 print issueid
3112 elif options.field == 'patch':
3113 patchset = cl.GetPatchset()
3114 if patchset:
3115 print patchset
3116 elif options.field == 'url':
3117 url = cl.GetIssueURL()
3118 if url:
3119 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003120 return 0
3121
3122 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3123 if not branches:
3124 print('No local branch found.')
3125 return 0
3126
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003127 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003128 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003129 for b in branches.splitlines()]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003130 print 'Branches associated with reviews:'
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003131 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003132 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003133 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003134
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003135 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003136 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3137 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3138 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003139 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003140 c, status = output.next()
3141 branch_statuses[c.GetBranch()] = status
3142 status = branch_statuses.pop(branch)
3143 url = cl.GetIssueURL()
3144 if url and (not status or status == 'error'):
3145 # The issue probably doesn't exist anymore.
3146 url += ' (broken)'
3147
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003148 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003149 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003150 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003151 color = ''
3152 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003153 status_str = '(%s)' % status if status else ''
3154 print ' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003155 alignment, ShortBranchName(branch), color, url,
3156 status_str, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003157
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003158 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003159 print
3160 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003161 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003162 if not cl.GetIssue():
3163 print 'No issue assigned.'
3164 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003165 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00003166 if not options.fast:
3167 print 'Issue description:'
3168 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003169 return 0
3170
3171
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003172def colorize_CMDstatus_doc():
3173 """To be called once in main() to add colors to git cl status help."""
3174 colors = [i for i in dir(Fore) if i[0].isupper()]
3175
3176 def colorize_line(line):
3177 for color in colors:
3178 if color in line.upper():
3179 # Extract whitespaces first and the leading '-'.
3180 indent = len(line) - len(line.lstrip(' ')) + 1
3181 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3182 return line
3183
3184 lines = CMDstatus.__doc__.splitlines()
3185 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3186
3187
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003188@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003189def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003190 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003191
3192 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003193 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003194 parser.add_option('-r', '--reverse', action='store_true',
3195 help='Lookup the branch(es) for the specified issues. If '
3196 'no issues are specified, all branches with mapped '
3197 'issues will be listed.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003198 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003199 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003200 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003201
dnj@chromium.org406c4402015-03-03 17:22:28 +00003202 if options.reverse:
3203 branches = RunGit(['for-each-ref', 'refs/heads',
3204 '--format=%(refname:short)']).splitlines()
3205
3206 # Reverse issue lookup.
3207 issue_branch_map = {}
3208 for branch in branches:
3209 cl = Changelist(branchref=branch)
3210 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3211 if not args:
3212 args = sorted(issue_branch_map.iterkeys())
3213 for issue in args:
3214 if not issue:
3215 continue
3216 print 'Branch for issue number %s: %s' % (
3217 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
3218 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003219 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003220 if len(args) > 0:
3221 try:
3222 issue = int(args[0])
3223 except ValueError:
3224 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003225 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003226 cl.SetIssue(issue)
3227 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003228 return 0
3229
3230
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003231def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003232 """Shows or posts review comments for any changelist."""
3233 parser.add_option('-a', '--add-comment', dest='comment',
3234 help='comment to add to an issue')
3235 parser.add_option('-i', dest='issue',
3236 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003237 parser.add_option('-j', '--json-file',
3238 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003239 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003240 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003241 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003242
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003243 issue = None
3244 if options.issue:
3245 try:
3246 issue = int(options.issue)
3247 except ValueError:
3248 DieWithError('A review issue id is expected to be a number')
3249
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003250 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003251
3252 if options.comment:
3253 cl.AddComment(options.comment)
3254 return 0
3255
3256 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003257 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003258 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003259 summary.append({
3260 'date': message['date'],
3261 'lgtm': False,
3262 'message': message['text'],
3263 'not_lgtm': False,
3264 'sender': message['sender'],
3265 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003266 if message['disapproval']:
3267 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003268 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003269 elif message['approval']:
3270 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003271 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003272 elif message['sender'] == data['owner_email']:
3273 color = Fore.MAGENTA
3274 else:
3275 color = Fore.BLUE
3276 print '\n%s%s %s%s' % (
3277 color, message['date'].split('.', 1)[0], message['sender'],
3278 Fore.RESET)
3279 if message['text'].strip():
3280 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00003281 if options.json_file:
3282 with open(options.json_file, 'wb') as f:
3283 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003284 return 0
3285
3286
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003287@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003288def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003289 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003290 parser.add_option('-d', '--display', action='store_true',
3291 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003292 parser.add_option('-n', '--new-description',
3293 help='New description to set for this issue (- for stdin)')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003294
3295 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003296 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003297 options, args = parser.parse_args(args)
3298 _process_codereview_select_options(parser, options)
3299
3300 target_issue = None
3301 if len(args) > 0:
3302 issue_arg = ParseIssueNumberArgument(args[0])
3303 if not issue_arg.valid:
3304 parser.print_help()
3305 return 1
3306 target_issue = issue_arg.issue
3307
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003308 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003309
3310 cl = Changelist(
3311 auth_config=auth_config, issue=target_issue,
3312 codereview=options.forced_codereview)
3313
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003314 if not cl.GetIssue():
3315 DieWithError('This branch has no associated changelist.')
3316 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003317
smut@google.com34fb6b12015-07-13 20:03:26 +00003318 if options.display:
tandrii@chromium.org8c3b4422016-04-27 13:11:18 +00003319 print description.description
smut@google.com34fb6b12015-07-13 20:03:26 +00003320 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003321
3322 if options.new_description:
3323 text = options.new_description
3324 if text == '-':
3325 text = '\n'.join(l.rstrip() for l in sys.stdin)
3326
3327 description.set_description(text)
3328 else:
3329 description.prompt()
3330
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003331 if cl.GetDescription() != description.description:
3332 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003333 return 0
3334
3335
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003336def CreateDescriptionFromLog(args):
3337 """Pulls out the commit log to use as a base for the CL description."""
3338 log_args = []
3339 if len(args) == 1 and not args[0].endswith('.'):
3340 log_args = [args[0] + '..']
3341 elif len(args) == 1 and args[0].endswith('...'):
3342 log_args = [args[0][:-1]]
3343 elif len(args) == 2:
3344 log_args = [args[0] + '..' + args[1]]
3345 else:
3346 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003347 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003348
3349
thestig@chromium.org44202a22014-03-11 19:22:18 +00003350def CMDlint(parser, args):
3351 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003352 parser.add_option('--filter', action='append', metavar='-x,+y',
3353 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003354 auth.add_auth_options(parser)
3355 options, args = parser.parse_args(args)
3356 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003357
3358 # Access to a protected member _XX of a client class
3359 # pylint: disable=W0212
3360 try:
3361 import cpplint
3362 import cpplint_chromium
3363 except ImportError:
3364 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
3365 return 1
3366
3367 # Change the current working directory before calling lint so that it
3368 # shows the correct base.
3369 previous_cwd = os.getcwd()
3370 os.chdir(settings.GetRoot())
3371 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003372 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003373 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3374 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003375 if not files:
3376 print "Cannot lint an empty CL"
3377 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003378
3379 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003380 command = args + files
3381 if options.filter:
3382 command = ['--filter=' + ','.join(options.filter)] + command
3383 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003384
3385 white_regex = re.compile(settings.GetLintRegex())
3386 black_regex = re.compile(settings.GetLintIgnoreRegex())
3387 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3388 for filename in filenames:
3389 if white_regex.match(filename):
3390 if black_regex.match(filename):
3391 print "Ignoring file %s" % filename
3392 else:
3393 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3394 extra_check_functions)
3395 else:
3396 print "Skipping file %s" % filename
3397 finally:
3398 os.chdir(previous_cwd)
3399 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
3400 if cpplint._cpplint_state.error_count != 0:
3401 return 1
3402 return 0
3403
3404
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003405def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003406 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003407 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003408 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003409 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003410 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003411 auth.add_auth_options(parser)
3412 options, args = parser.parse_args(args)
3413 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003414
sbc@chromium.org71437c02015-04-09 19:29:40 +00003415 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00003416 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003417 return 1
3418
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003419 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003420 if args:
3421 base_branch = args[0]
3422 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003423 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003424 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003425
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003426 cl.RunHook(
3427 committing=not options.upload,
3428 may_prompt=False,
3429 verbose=options.verbose,
3430 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003431 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003432
3433
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003434def GenerateGerritChangeId(message):
3435 """Returns Ixxxxxx...xxx change id.
3436
3437 Works the same way as
3438 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3439 but can be called on demand on all platforms.
3440
3441 The basic idea is to generate git hash of a state of the tree, original commit
3442 message, author/committer info and timestamps.
3443 """
3444 lines = []
3445 tree_hash = RunGitSilent(['write-tree'])
3446 lines.append('tree %s' % tree_hash.strip())
3447 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3448 if code == 0:
3449 lines.append('parent %s' % parent.strip())
3450 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3451 lines.append('author %s' % author.strip())
3452 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3453 lines.append('committer %s' % committer.strip())
3454 lines.append('')
3455 # Note: Gerrit's commit-hook actually cleans message of some lines and
3456 # whitespace. This code is not doing this, but it clearly won't decrease
3457 # entropy.
3458 lines.append(message)
3459 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3460 stdin='\n'.join(lines))
3461 return 'I%s' % change_hash.strip()
3462
3463
wittman@chromium.org455dc922015-01-26 20:15:50 +00003464def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3465 """Computes the remote branch ref to use for the CL.
3466
3467 Args:
3468 remote (str): The git remote for the CL.
3469 remote_branch (str): The git remote branch for the CL.
3470 target_branch (str): The target branch specified by the user.
3471 pending_prefix (str): The pending prefix from the settings.
3472 """
3473 if not (remote and remote_branch):
3474 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003475
wittman@chromium.org455dc922015-01-26 20:15:50 +00003476 if target_branch:
3477 # Cannonicalize branch references to the equivalent local full symbolic
3478 # refs, which are then translated into the remote full symbolic refs
3479 # below.
3480 if '/' not in target_branch:
3481 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3482 else:
3483 prefix_replacements = (
3484 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3485 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3486 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3487 )
3488 match = None
3489 for regex, replacement in prefix_replacements:
3490 match = re.search(regex, target_branch)
3491 if match:
3492 remote_branch = target_branch.replace(match.group(0), replacement)
3493 break
3494 if not match:
3495 # This is a branch path but not one we recognize; use as-is.
3496 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003497 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3498 # Handle the refs that need to land in different refs.
3499 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003500
wittman@chromium.org455dc922015-01-26 20:15:50 +00003501 # Create the true path to the remote branch.
3502 # Does the following translation:
3503 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3504 # * refs/remotes/origin/master -> refs/heads/master
3505 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3506 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3507 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3508 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3509 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3510 'refs/heads/')
3511 elif remote_branch.startswith('refs/remotes/branch-heads'):
3512 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3513 # If a pending prefix exists then replace refs/ with it.
3514 if pending_prefix:
3515 remote_branch = remote_branch.replace('refs/', pending_prefix)
3516 return remote_branch
3517
3518
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003519def cleanup_list(l):
3520 """Fixes a list so that comma separated items are put as individual items.
3521
3522 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3523 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3524 """
3525 items = sum((i.split(',') for i in l), [])
3526 stripped_items = (i.strip() for i in items)
3527 return sorted(filter(None, stripped_items))
3528
3529
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003530@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003531def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003532 """Uploads the current changelist to codereview.
3533
3534 Can skip dependency patchset uploads for a branch by running:
3535 git config branch.branch_name.skip-deps-uploads True
3536 To unset run:
3537 git config --unset branch.branch_name.skip-deps-uploads
3538 Can also set the above globally by using the --global flag.
3539 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003540 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3541 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003542 parser.add_option('--bypass-watchlists', action='store_true',
3543 dest='bypass_watchlists',
3544 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003545 parser.add_option('-f', action='store_true', dest='force',
3546 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003547 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003548 parser.add_option('-t', dest='title',
3549 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003550 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003551 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003552 help='reviewer email addresses')
3553 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003554 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003555 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003556 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003557 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003558 parser.add_option('--emulate_svn_auto_props',
3559 '--emulate-svn-auto-props',
3560 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003561 dest="emulate_svn_auto_props",
3562 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003563 parser.add_option('-c', '--use-commit-queue', action='store_true',
3564 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003565 parser.add_option('--private', action='store_true',
3566 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003567 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003568 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003569 metavar='TARGET',
3570 help='Apply CL to remote ref TARGET. ' +
3571 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003572 parser.add_option('--squash', action='store_true',
3573 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003574 parser.add_option('--no-squash', action='store_true',
3575 help='Don\'t squash multiple commits into one ' +
3576 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003577 parser.add_option('--email', default=None,
3578 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003579 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3580 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003581 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3582 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003583 help='Send the patchset to do a CQ dry run right after '
3584 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003585 parser.add_option('--dependencies', action='store_true',
3586 help='Uploads CLs of all the local branches that depend on '
3587 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003588
rmistry@google.com2dd99862015-06-22 12:22:18 +00003589 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003590 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003591 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003592 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003593 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003594 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003595 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003596
sbc@chromium.org71437c02015-04-09 19:29:40 +00003597 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003598 return 1
3599
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003600 options.reviewers = cleanup_list(options.reviewers)
3601 options.cc = cleanup_list(options.cc)
3602
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003603 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3604 settings.GetIsGerrit()
3605
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003606 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00003607 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003608
3609
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003610def IsSubmoduleMergeCommit(ref):
3611 # When submodules are added to the repo, we expect there to be a single
3612 # non-git-svn merge commit at remote HEAD with a signature comment.
3613 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003614 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003615 return RunGit(cmd) != ''
3616
3617
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003618def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003619 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003620
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003621 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3622 upstream and closes the issue automatically and atomically.
3623
3624 Otherwise (in case of Rietveld):
3625 Squashes branch into a single commit.
3626 Updates changelog with metadata (e.g. pointer to review).
3627 Pushes/dcommits the code upstream.
3628 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003629 """
3630 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3631 help='bypass upload presubmit hook')
3632 parser.add_option('-m', dest='message',
3633 help="override review description")
3634 parser.add_option('-f', action='store_true', dest='force',
3635 help="force yes to questions (don't prompt)")
3636 parser.add_option('-c', dest='contributor',
3637 help="external contributor for patch (appended to " +
3638 "description and used as author for git). Should be " +
3639 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003640 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003641 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003642 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003643 auth_config = auth.extract_auth_config_from_options(options)
3644
3645 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003646
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003647 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3648 if cl.IsGerrit():
3649 if options.message:
3650 # This could be implemented, but it requires sending a new patch to
3651 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3652 # Besides, Gerrit has the ability to change the commit message on submit
3653 # automatically, thus there is no need to support this option (so far?).
3654 parser.error('-m MESSAGE option is not supported for Gerrit.')
3655 if options.contributor:
3656 parser.error(
3657 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3658 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3659 'the contributor\'s "name <email>". If you can\'t upload such a '
3660 'commit for review, contact your repository admin and request'
3661 '"Forge-Author" permission.')
3662 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3663 options.verbose)
3664
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003665 current = cl.GetBranch()
3666 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3667 if not settings.GetIsGitSvn() and remote == '.':
3668 print
3669 print 'Attempting to push branch %r into another local branch!' % current
3670 print
3671 print 'Either reparent this branch on top of origin/master:'
3672 print ' git reparent-branch --root'
3673 print
3674 print 'OR run `git rebase-update` if you think the parent branch is already'
3675 print 'committed.'
3676 print
3677 print ' Current parent: %r' % upstream_branch
3678 return 1
3679
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003680 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003681 # Default to merging against our best guess of the upstream branch.
3682 args = [cl.GetUpstreamBranch()]
3683
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003684 if options.contributor:
3685 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
3686 print "Please provide contibutor as 'First Last <email@example.com>'"
3687 return 1
3688
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003689 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003690 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003691
sbc@chromium.org71437c02015-04-09 19:29:40 +00003692 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003693 return 1
3694
3695 # This rev-list syntax means "show all commits not in my branch that
3696 # are in base_branch".
3697 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3698 base_branch]).splitlines()
3699 if upstream_commits:
3700 print ('Base branch "%s" has %d commits '
3701 'not in this branch.' % (base_branch, len(upstream_commits)))
3702 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
3703 return 1
3704
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003705 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003706 svn_head = None
3707 if cmd == 'dcommit' or base_has_submodules:
3708 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
3709 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003710
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003711 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003712 # If the base_head is a submodule merge commit, the first parent of the
3713 # base_head should be a git-svn commit, which is what we're interested in.
3714 base_svn_head = base_branch
3715 if base_has_submodules:
3716 base_svn_head += '^1'
3717
3718 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003719 if extra_commits:
3720 print ('This branch has %d additional commits not upstreamed yet.'
3721 % len(extra_commits.splitlines()))
3722 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
3723 'before attempting to %s.' % (base_branch, cmd))
3724 return 1
3725
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003726 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003727 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003728 author = None
3729 if options.contributor:
3730 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003731 hook_results = cl.RunHook(
3732 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003733 may_prompt=not options.force,
3734 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003735 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003736 if not hook_results.should_continue():
3737 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003738
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003739 # Check the tree status if the tree status URL is set.
3740 status = GetTreeStatus()
3741 if 'closed' == status:
3742 print('The tree is closed. Please wait for it to reopen. Use '
3743 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3744 return 1
3745 elif 'unknown' == status:
3746 print('Unable to determine tree status. Please verify manually and '
3747 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3748 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003749
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003750 change_desc = ChangeDescription(options.message)
3751 if not change_desc.description and cl.GetIssue():
3752 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003753
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003754 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00003755 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003756 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00003757 else:
3758 print 'No description set.'
3759 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
3760 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003761
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003762 # Keep a separate copy for the commit message, because the commit message
3763 # contains the link to the Rietveld issue, while the Rietveld message contains
3764 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003765 # Keep a separate copy for the commit message.
3766 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00003767 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003768
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003769 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00003770 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00003771 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00003772 # after it. Add a period on a new line to circumvent this. Also add a space
3773 # before the period to make sure that Gitiles continues to correctly resolve
3774 # the URL.
3775 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003776 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003777 commit_desc.append_footer('Patch from %s.' % options.contributor)
3778
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00003779 print('Description:')
3780 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003781
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003782 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003783 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00003784 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003785
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003786 # We want to squash all this branch's commits into one commit with the proper
3787 # description. We do this by doing a "reset --soft" to the base branch (which
3788 # keeps the working copy the same), then dcommitting that. If origin/master
3789 # has a submodule merge commit, we'll also need to cherry-pick the squashed
3790 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003791 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003792 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
3793 # Delete the branches if they exist.
3794 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
3795 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
3796 result = RunGitWithCode(showref_cmd)
3797 if result[0] == 0:
3798 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003799
3800 # We might be in a directory that's present in this branch but not in the
3801 # trunk. Move up to the top of the tree so that git commands that expect a
3802 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003803 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003804 if rel_base_path:
3805 os.chdir(rel_base_path)
3806
3807 # Stuff our change into the merge branch.
3808 # We wrap in a try...finally block so if anything goes wrong,
3809 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003810 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003811 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003812 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003813 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003814 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00003815 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003816 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003817 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003818 RunGit(
3819 [
3820 'commit', '--author', options.contributor,
3821 '-m', commit_desc.description,
3822 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003823 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003824 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003825 if base_has_submodules:
3826 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
3827 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
3828 RunGit(['checkout', CHERRY_PICK_BRANCH])
3829 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003830 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003831 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003832 mirror = settings.GetGitMirror(remote)
3833 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003834 pending_prefix = settings.GetPendingRefPrefix()
3835 if not pending_prefix or branch.startswith(pending_prefix):
3836 # If not using refs/pending/heads/* at all, or target ref is already set
3837 # to pending, then push to the target ref directly.
3838 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003839 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003840 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003841 else:
3842 # Cherry-pick the change on top of pending ref and then push it.
3843 assert branch.startswith('refs/'), branch
3844 assert pending_prefix[-1] == '/', pending_prefix
3845 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003846 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003847 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003848 if retcode == 0:
3849 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003850 else:
3851 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003852 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003853 'svn', 'dcommit',
3854 '-C%s' % options.similarity,
3855 '--no-rebase', '--rmdir',
3856 ]
3857 if settings.GetForceHttpsCommitUrl():
3858 # Allow forcing https commit URLs for some projects that don't allow
3859 # committing to http URLs (like Google Code).
3860 remote_url = cl.GetGitSvnRemoteUrl()
3861 if urlparse.urlparse(remote_url).scheme == 'http':
3862 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003863 cmd_args.append('--commit-url=%s' % remote_url)
3864 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003865 if 'Committed r' in output:
3866 revision = re.match(
3867 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
3868 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003869 finally:
3870 # And then swap back to the original branch and clean up.
3871 RunGit(['checkout', '-q', cl.GetBranch()])
3872 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003873 if base_has_submodules:
3874 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003875
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003876 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003877 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003878 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003879
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003880 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003881 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003882 try:
3883 revision = WaitForRealCommit(remote, revision, base_branch, branch)
3884 # We set pushed_to_pending to False, since it made it all the way to the
3885 # real ref.
3886 pushed_to_pending = False
3887 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003888 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003889
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003890 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003891 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003892 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003893 if not to_pending:
3894 if viewvc_url and revision:
3895 change_desc.append_footer(
3896 'Committed: %s%s' % (viewvc_url, revision))
3897 elif revision:
3898 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003899 print ('Closing issue '
3900 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003901 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003902 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003903 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00003904 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00003905 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00003906 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003907 if options.bypass_hooks:
3908 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
3909 else:
3910 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00003911 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003912 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003913
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003914 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003915 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
3916 print 'The commit is in the pending queue (%s).' % pending_ref
3917 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00003918 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003919 'footer.' % branch)
3920
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003921 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
3922 if os.path.isfile(hook):
3923 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003924
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003925 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003926
3927
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003928def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
3929 print
3930 print 'Waiting for commit to be landed on %s...' % real_ref
3931 print '(If you are impatient, you may Ctrl-C once without harm)'
3932 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
3933 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003934 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003935
3936 loop = 0
3937 while True:
3938 sys.stdout.write('fetching (%d)... \r' % loop)
3939 sys.stdout.flush()
3940 loop += 1
3941
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003942 if mirror:
3943 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003944 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
3945 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
3946 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
3947 for commit in commits.splitlines():
3948 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
3949 print 'Found commit on %s' % real_ref
3950 return commit
3951
3952 current_rev = to_rev
3953
3954
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003955def PushToGitPending(remote, pending_ref, upstream_ref):
3956 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
3957
3958 Returns:
3959 (retcode of last operation, output log of last operation).
3960 """
3961 assert pending_ref.startswith('refs/'), pending_ref
3962 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
3963 cherry = RunGit(['rev-parse', 'HEAD']).strip()
3964 code = 0
3965 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003966 max_attempts = 3
3967 attempts_left = max_attempts
3968 while attempts_left:
3969 if attempts_left != max_attempts:
3970 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
3971 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003972
3973 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003974 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003975 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003976 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003977 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003978 print 'Fetch failed with exit code %d.' % code
3979 if out.strip():
3980 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003981 continue
3982
3983 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003984 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003985 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003986 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003987 if code:
3988 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003989 'Your patch doesn\'t apply cleanly to ref \'%s\', '
3990 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003991 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
3992 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003993 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003994 return code, out
3995
3996 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003997 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003998 code, out = RunGitWithCode(
3999 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4000 if code == 0:
4001 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004002 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004003 return code, out
4004
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004005 print 'Push failed with exit code %d.' % code
4006 if out.strip():
4007 print out.strip()
4008 if IsFatalPushFailure(out):
4009 print (
4010 'Fatal push error. Make sure your .netrc credentials and git '
4011 'user.email are correct and you have push access to the repo.')
4012 return code, out
4013
4014 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004015 return code, out
4016
4017
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004018def IsFatalPushFailure(push_stdout):
4019 """True if retrying push won't help."""
4020 return '(prohibited by Gerrit)' in push_stdout
4021
4022
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004023@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004024def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004025 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004026 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004027 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004028 # If it looks like previous commits were mirrored with git-svn.
4029 message = """This repository appears to be a git-svn mirror, but no
4030upstream SVN master is set. You probably need to run 'git auto-svn' once."""
4031 else:
4032 message = """This doesn't appear to be an SVN repository.
4033If your project has a true, writeable git repository, you probably want to run
4034'git cl land' instead.
4035If your project has a git mirror of an upstream SVN master, you probably need
4036to run 'git svn init'.
4037
4038Using the wrong command might cause your commit to appear to succeed, and the
4039review to be closed, without actually landing upstream. If you choose to
4040proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004041 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004042 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004043 return SendUpstream(parser, args, 'dcommit')
4044
4045
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004046@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004047def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004048 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004049 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004050 print('This appears to be an SVN repository.')
4051 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004052 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004053 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004054 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004055
4056
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004057@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004058def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004059 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004060 parser.add_option('-b', dest='newbranch',
4061 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004062 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004063 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004064 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4065 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004066 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004067 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004068 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004069 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004070 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004071 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004072
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004073
4074 group = optparse.OptionGroup(
4075 parser,
4076 'Options for continuing work on the current issue uploaded from a '
4077 'different clone (e.g. different machine). Must be used independently '
4078 'from the other options. No issue number should be specified, and the '
4079 'branch must have an issue number associated with it')
4080 group.add_option('--reapply', action='store_true', dest='reapply',
4081 help='Reset the branch and reapply the issue.\n'
4082 'CAUTION: This will undo any local changes in this '
4083 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004084
4085 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004086 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004087 parser.add_option_group(group)
4088
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004089 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004090 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004091 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004092 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004093 auth_config = auth.extract_auth_config_from_options(options)
4094
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004095 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004096
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004097 issue_arg = None
4098 if options.reapply :
4099 if len(args) > 0:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004100 parser.error('--reapply implies no additional arguments.')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004101
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004102 issue_arg = cl.GetIssue()
4103 upstream = cl.GetUpstreamBranch()
4104 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004105 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004106
4107 RunGit(['reset', '--hard', upstream])
4108 if options.pull:
4109 RunGit(['pull'])
4110 else:
4111 if len(args) != 1:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004112 parser.error('Must specify issue number or url')
4113 issue_arg = args[0]
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004114
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004115 if not issue_arg:
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004116 parser.print_help()
4117 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004118
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004119 if cl.IsGerrit():
4120 if options.reject:
4121 parser.error('--reject is not supported with Gerrit codereview.')
4122 if options.nocommit:
4123 parser.error('--nocommit is not supported with Gerrit codereview.')
4124 if options.directory:
4125 parser.error('--directory is not supported with Gerrit codereview.')
4126
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004127 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004128 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004129 return 1
4130
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004131 if options.newbranch:
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004132 if options.reapply:
4133 parser.error("--reapply excludes any option other than --pull")
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004134 if options.force:
4135 RunGit(['branch', '-D', options.newbranch],
4136 stderr=subprocess2.PIPE, error_ok=True)
4137 RunGit(['checkout', '-b', options.newbranch,
4138 Changelist().GetUpstreamBranch()])
4139
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004140 return cl.CMDPatchIssue(issue_arg, options.reject, options.nocommit,
4141 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004142
4143
4144def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004145 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004146 # Provide a wrapper for git svn rebase to help avoid accidental
4147 # git svn dcommit.
4148 # It's the only command that doesn't use parser at all since we just defer
4149 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004150
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004151 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004152
4153
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004154def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004155 """Fetches the tree status and returns either 'open', 'closed',
4156 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004157 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004158 if url:
4159 status = urllib2.urlopen(url).read().lower()
4160 if status.find('closed') != -1 or status == '0':
4161 return 'closed'
4162 elif status.find('open') != -1 or status == '1':
4163 return 'open'
4164 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004165 return 'unset'
4166
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004167
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004168def GetTreeStatusReason():
4169 """Fetches the tree status from a json url and returns the message
4170 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004171 url = settings.GetTreeStatusUrl()
4172 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004173 connection = urllib2.urlopen(json_url)
4174 status = json.loads(connection.read())
4175 connection.close()
4176 return status['message']
4177
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004178
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004179def GetBuilderMaster(bot_list):
4180 """For a given builder, fetch the master from AE if available."""
4181 map_url = 'https://builders-map.appspot.com/'
4182 try:
4183 master_map = json.load(urllib2.urlopen(map_url))
4184 except urllib2.URLError as e:
4185 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4186 (map_url, e))
4187 except ValueError as e:
4188 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4189 if not master_map:
4190 return None, 'Failed to build master map.'
4191
4192 result_master = ''
4193 for bot in bot_list:
4194 builder = bot.split(':', 1)[0]
4195 master_list = master_map.get(builder, [])
4196 if not master_list:
4197 return None, ('No matching master for builder %s.' % builder)
4198 elif len(master_list) > 1:
4199 return None, ('The builder name %s exists in multiple masters %s.' %
4200 (builder, master_list))
4201 else:
4202 cur_master = master_list[0]
4203 if not result_master:
4204 result_master = cur_master
4205 elif result_master != cur_master:
4206 return None, 'The builders do not belong to the same master.'
4207 return result_master, None
4208
4209
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004210def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004211 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004212 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004213 status = GetTreeStatus()
4214 if 'unset' == status:
4215 print 'You must configure your tree status URL by running "git cl config".'
4216 return 2
4217
4218 print "The tree is %s" % status
4219 print
4220 print GetTreeStatusReason()
4221 if status != 'open':
4222 return 1
4223 return 0
4224
4225
maruel@chromium.org15192402012-09-06 12:38:29 +00004226def CMDtry(parser, args):
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004227 """Triggers try jobs through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00004228 group = optparse.OptionGroup(parser, "Try job options")
4229 group.add_option(
4230 "-b", "--bot", action="append",
4231 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
4232 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004233 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00004234 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004235 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00004236 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004237 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00004238 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004239 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004240 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004241 "-r", "--revision",
4242 help="Revision to use for the try job; default: the "
4243 "revision will be determined by the try server; see "
4244 "its waterfall for more info")
4245 group.add_option(
4246 "-c", "--clobber", action="store_true", default=False,
4247 help="Force a clobber before building; e.g. don't do an "
4248 "incremental build")
4249 group.add_option(
4250 "--project",
4251 help="Override which project to use. Projects are defined "
4252 "server-side to define what default bot set to use")
4253 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004254 "-p", "--property", dest="properties", action="append", default=[],
4255 help="Specify generic properties in the form -p key1=value1 -p "
4256 "key2=value2 etc (buildbucket only). The value will be treated as "
4257 "json if decodable, or as string otherwise.")
4258 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004259 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004260 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004261 "--use-rietveld", action="store_true", default=False,
4262 help="Use Rietveld to trigger try jobs.")
4263 group.add_option(
4264 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4265 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004266 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004267 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004268 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004269 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004270
machenbach@chromium.org45453142015-09-15 08:45:22 +00004271 if options.use_rietveld and options.properties:
4272 parser.error('Properties can only be specified with buildbucket')
4273
4274 # Make sure that all properties are prop=value pairs.
4275 bad_params = [x for x in options.properties if '=' not in x]
4276 if bad_params:
4277 parser.error('Got properties with missing "=": %s' % bad_params)
4278
maruel@chromium.org15192402012-09-06 12:38:29 +00004279 if args:
4280 parser.error('Unknown arguments: %s' % args)
4281
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004282 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004283 if not cl.GetIssue():
4284 parser.error('Need to upload first')
4285
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004286 if cl.IsGerrit():
4287 parser.error(
4288 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4289 'If your project has Commit Queue, dry run is a workaround:\n'
4290 ' git cl set-commit --dry-run')
4291 # Code below assumes Rietveld issue.
4292 # TODO(tandrii): actually implement for Gerrit http://crbug.com/599931.
4293
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004294 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004295 if props.get('closed'):
4296 parser.error('Cannot send tryjobs for a closed CL')
4297
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004298 if props.get('private'):
4299 parser.error('Cannot use trybots with private issue')
4300
maruel@chromium.org15192402012-09-06 12:38:29 +00004301 if not options.name:
4302 options.name = cl.GetBranch()
4303
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004304 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004305 options.master, err_msg = GetBuilderMaster(options.bot)
4306 if err_msg:
4307 parser.error('Tryserver master cannot be found because: %s\n'
4308 'Please manually specify the tryserver master'
4309 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004310
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004311 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004312 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004313 if not options.bot:
4314 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004315
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004316 # Get try masters from PRESUBMIT.py files.
4317 masters = presubmit_support.DoGetTryMasters(
4318 change,
4319 change.LocalPaths(),
4320 settings.GetRoot(),
4321 None,
4322 None,
4323 options.verbose,
4324 sys.stdout)
4325 if masters:
4326 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004327
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004328 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4329 options.bot = presubmit_support.DoGetTrySlaves(
4330 change,
4331 change.LocalPaths(),
4332 settings.GetRoot(),
4333 None,
4334 None,
4335 options.verbose,
4336 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004337
4338 if not options.bot:
4339 # Get try masters from cq.cfg if any.
4340 # TODO(tandrii): some (but very few) projects store cq.cfg in different
4341 # location.
4342 cq_cfg = os.path.join(change.RepositoryRoot(),
4343 'infra', 'config', 'cq.cfg')
4344 if os.path.exists(cq_cfg):
4345 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00004346 cq_masters = commit_queue.get_master_builder_map(
4347 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004348 for master, builders in cq_masters.iteritems():
4349 for builder in builders:
4350 # Skip presubmit builders, because these will fail without LGTM.
machenbach@chromium.org2403e802016-04-29 12:34:42 +00004351 masters.setdefault(master, {})[builder] = ['defaulttests']
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004352 if masters:
4353 return masters
4354
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004355 if not options.bot:
4356 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00004357
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004358 builders_and_tests = {}
4359 # TODO(machenbach): The old style command-line options don't support
4360 # multiple try masters yet.
4361 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4362 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4363
4364 for bot in old_style:
4365 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004366 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004367 elif ',' in bot:
4368 parser.error('Specify one bot per --bot flag')
4369 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004370 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004371
4372 for bot, tests in new_style:
4373 builders_and_tests.setdefault(bot, []).extend(tests)
4374
4375 # Return a master map with one master to be backwards compatible. The
4376 # master name defaults to an empty string, which will cause the master
4377 # not to be set on rietveld (deprecated).
4378 return {options.master: builders_and_tests}
4379
4380 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004381
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004382 for builders in masters.itervalues():
4383 if any('triggered' in b for b in builders):
4384 print >> sys.stderr, (
4385 'ERROR You are trying to send a job to a triggered bot. This type of'
4386 ' bot requires an\ninitial job from a parent (usually a builder). '
4387 'Instead send your job to the parent.\n'
4388 'Bot list: %s' % builders)
4389 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004390
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004391 patchset = cl.GetMostRecentPatchset()
4392 if patchset and patchset != cl.GetPatchset():
4393 print(
4394 '\nWARNING Mismatch between local config and server. Did a previous '
4395 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4396 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004397 if options.luci:
4398 trigger_luci_job(cl, masters, options)
4399 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004400 try:
4401 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4402 except BuildbucketResponseException as ex:
4403 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004404 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004405 except Exception as e:
4406 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4407 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
4408 e, stacktrace)
4409 return 1
4410 else:
4411 try:
4412 cl.RpcServer().trigger_distributed_try_jobs(
4413 cl.GetIssue(), patchset, options.name, options.clobber,
4414 options.revision, masters)
4415 except urllib2.HTTPError as e:
4416 if e.code == 404:
4417 print('404 from rietveld; '
4418 'did you mean to use "git try" instead of "git cl try"?')
4419 return 1
4420 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004421
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004422 for (master, builders) in sorted(masters.iteritems()):
4423 if master:
4424 print 'Master: %s' % master
4425 length = max(len(builder) for builder in builders)
4426 for builder in sorted(builders):
4427 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00004428 return 0
4429
4430
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004431def CMDtry_results(parser, args):
4432 group = optparse.OptionGroup(parser, "Try job results options")
4433 group.add_option(
4434 "-p", "--patchset", type=int, help="patchset number if not current.")
4435 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004436 "--print-master", action='store_true', help="print master name as well.")
4437 group.add_option(
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004438 "--color", action='store_true', default=setup_color.IS_TTY,
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004439 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004440 group.add_option(
4441 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4442 help="Host of buildbucket. The default host is %default.")
4443 parser.add_option_group(group)
4444 auth.add_auth_options(parser)
4445 options, args = parser.parse_args(args)
4446 if args:
4447 parser.error('Unrecognized args: %s' % ' '.join(args))
4448
4449 auth_config = auth.extract_auth_config_from_options(options)
4450 cl = Changelist(auth_config=auth_config)
4451 if not cl.GetIssue():
4452 parser.error('Need to upload first')
4453
4454 if not options.patchset:
4455 options.patchset = cl.GetMostRecentPatchset()
4456 if options.patchset and options.patchset != cl.GetPatchset():
4457 print(
4458 '\nWARNING Mismatch between local config and server. Did a previous '
4459 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4460 'Continuing using\npatchset %s.\n' % options.patchset)
4461 try:
4462 jobs = fetch_try_jobs(auth_config, cl, options)
4463 except BuildbucketResponseException as ex:
4464 print 'Buildbucket error: %s' % ex
4465 return 1
4466 except Exception as e:
4467 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4468 print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
4469 e, stacktrace)
4470 return 1
4471 print_tryjobs(options, jobs)
4472 return 0
4473
4474
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004475@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004476def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004477 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004478 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004479 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004480 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004481
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004482 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004483 if args:
4484 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004485 branch = cl.GetBranch()
4486 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004487 cl = Changelist()
4488 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004489
4490 # Clear configured merge-base, if there is one.
4491 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004492 else:
4493 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004494 return 0
4495
4496
thestig@chromium.org00858c82013-12-02 23:08:03 +00004497def CMDweb(parser, args):
4498 """Opens the current CL in the web browser."""
4499 _, args = parser.parse_args(args)
4500 if args:
4501 parser.error('Unrecognized args: %s' % ' '.join(args))
4502
4503 issue_url = Changelist().GetIssueURL()
4504 if not issue_url:
4505 print >> sys.stderr, 'ERROR No issue to open'
4506 return 1
4507
4508 webbrowser.open(issue_url)
4509 return 0
4510
4511
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004512def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004513 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004514 parser.add_option('-d', '--dry-run', action='store_true',
4515 help='trigger in dry run mode')
4516 parser.add_option('-c', '--clear', action='store_true',
4517 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004518 auth.add_auth_options(parser)
4519 options, args = parser.parse_args(args)
4520 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004521 if args:
4522 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004523 if options.dry_run and options.clear:
4524 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4525
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004526 cl = Changelist(auth_config=auth_config)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004527 if options.clear:
4528 state = _CQState.CLEAR
4529 elif options.dry_run:
4530 state = _CQState.DRY_RUN
4531 else:
4532 state = _CQState.COMMIT
4533 if not cl.GetIssue():
4534 parser.error('Must upload the issue first')
4535 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004536 return 0
4537
4538
groby@chromium.org411034a2013-02-26 15:12:01 +00004539def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004540 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004541 auth.add_auth_options(parser)
4542 options, args = parser.parse_args(args)
4543 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004544 if args:
4545 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004546 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00004547 # Ensure there actually is an issue to close.
4548 cl.GetDescription()
4549 cl.CloseIssue()
4550 return 0
4551
4552
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004553def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004554 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004555 auth.add_auth_options(parser)
4556 options, args = parser.parse_args(args)
4557 auth_config = auth.extract_auth_config_from_options(options)
4558 if args:
4559 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004560
4561 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004562 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004563 # Staged changes would be committed along with the patch from last
4564 # upload, hence counted toward the "last upload" side in the final
4565 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004566 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004567 return 1
4568
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004569 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004570 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004571 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004572 if not issue:
4573 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004574 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004575 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004576
4577 # Create a new branch based on the merge-base
4578 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004579 # Clear cached branch in cl object, to avoid overwriting original CL branch
4580 # properties.
4581 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004582 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004583 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004584 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004585 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004586 return rtn
4587
wychen@chromium.org06928532015-02-03 02:11:29 +00004588 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004589 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00004590 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004591 finally:
4592 RunGit(['checkout', '-q', branch])
4593 RunGit(['branch', '-D', TMP_BRANCH])
4594
4595 return 0
4596
4597
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004598def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004599 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004600 parser.add_option(
4601 '--no-color',
4602 action='store_true',
4603 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004604 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004605 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004606 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004607
4608 author = RunGit(['config', 'user.email']).strip() or None
4609
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004610 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004611
4612 if args:
4613 if len(args) > 1:
4614 parser.error('Unknown args')
4615 base_branch = args[0]
4616 else:
4617 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004618 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004619
4620 change = cl.GetChange(base_branch, None)
4621 return owners_finder.OwnersFinder(
4622 [f.LocalPath() for f in
4623 cl.GetChange(base_branch, None).AffectedFiles()],
4624 change.RepositoryRoot(), author,
4625 fopen=file, os_path=os.path, glob=glob.glob,
4626 disable_color=options.no_color).run()
4627
4628
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004629def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004630 """Generates a diff command."""
4631 # Generate diff for the current branch's changes.
4632 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4633 upstream_commit, '--' ]
4634
4635 if args:
4636 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004637 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004638 diff_cmd.append(arg)
4639 else:
4640 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004641
4642 return diff_cmd
4643
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004644def MatchingFileType(file_name, extensions):
4645 """Returns true if the file name ends with one of the given extensions."""
4646 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004647
enne@chromium.org555cfe42014-01-29 18:21:39 +00004648@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004649def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004650 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00004651 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004652 GN_EXTS = ['.gn', '.gni']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004653 parser.add_option('--full', action='store_true',
4654 help='Reformat the full content of all touched files')
4655 parser.add_option('--dry-run', action='store_true',
4656 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004657 parser.add_option('--python', action='store_true',
4658 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004659 parser.add_option('--diff', action='store_true',
4660 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004661 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004662
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004663 # git diff generates paths against the root of the repository. Change
4664 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004665 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004666 if rel_base_path:
4667 os.chdir(rel_base_path)
4668
digit@chromium.org29e47272013-05-17 17:01:46 +00004669 # Grab the merge-base commit, i.e. the upstream commit of the current
4670 # branch when it was created or the last time it was rebased. This is
4671 # to cover the case where the user may have called "git fetch origin",
4672 # moving the origin branch to a newer commit, but hasn't rebased yet.
4673 upstream_commit = None
4674 cl = Changelist()
4675 upstream_branch = cl.GetUpstreamBranch()
4676 if upstream_branch:
4677 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4678 upstream_commit = upstream_commit.strip()
4679
4680 if not upstream_commit:
4681 DieWithError('Could not find base commit for this branch. '
4682 'Are you in detached state?')
4683
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004684 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4685 diff_output = RunGit(changed_files_cmd)
4686 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004687 # Filter out files deleted by this CL
4688 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004689
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004690 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4691 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4692 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004693 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004694
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004695 top_dir = os.path.normpath(
4696 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
4697
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004698 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4699 # formatted. This is used to block during the presubmit.
4700 return_value = 0
4701
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004702 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004703 # Locate the clang-format binary in the checkout
4704 try:
4705 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
4706 except clang_format.NotFoundError, e:
4707 DieWithError(e)
4708
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004709 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004710 cmd = [clang_format_tool]
4711 if not opts.dry_run and not opts.diff:
4712 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004713 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004714 if opts.diff:
4715 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004716 else:
4717 env = os.environ.copy()
4718 env['PATH'] = str(os.path.dirname(clang_format_tool))
4719 try:
4720 script = clang_format.FindClangFormatScriptInChromiumTree(
4721 'clang-format-diff.py')
4722 except clang_format.NotFoundError, e:
4723 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004724
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004725 cmd = [sys.executable, script, '-p0']
4726 if not opts.dry_run and not opts.diff:
4727 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004728
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004729 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
4730 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004731
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004732 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
4733 if opts.diff:
4734 sys.stdout.write(stdout)
4735 if opts.dry_run and len(stdout) > 0:
4736 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004737
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004738 # Similar code to above, but using yapf on .py files rather than clang-format
4739 # on C/C++ files
4740 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004741 yapf_tool = gclient_utils.FindExecutable('yapf')
4742 if yapf_tool is None:
4743 DieWithError('yapf not found in PATH')
4744
4745 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004746 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004747 cmd = [yapf_tool]
4748 if not opts.dry_run and not opts.diff:
4749 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004750 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004751 if opts.diff:
4752 sys.stdout.write(stdout)
4753 else:
4754 # TODO(sbc): yapf --lines mode still has some issues.
4755 # https://github.com/google/yapf/issues/154
4756 DieWithError('--python currently only works with --full')
4757
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004758 # Dart's formatter does not have the nice property of only operating on
4759 # modified chunks, so hard code full.
4760 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004761 try:
4762 command = [dart_format.FindDartFmtToolInChromiumTree()]
4763 if not opts.dry_run and not opts.diff:
4764 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004765 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004766
ppi@chromium.org6593d932016-03-03 15:41:15 +00004767 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004768 if opts.dry_run and stdout:
4769 return_value = 2
4770 except dart_format.NotFoundError as e:
erikcorry@chromium.org3e445022015-12-17 09:07:26 +00004771 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
4772 'found in this checkout. Files in other languages are still ' +
4773 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004774
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004775 # Format GN build files. Always run on full build files for canonical form.
4776 if gn_diff_files:
4777 cmd = ['gn', 'format']
4778 if not opts.dry_run and not opts.diff:
4779 cmd.append('--in-place')
4780 for gn_diff_file in gn_diff_files:
bsep@chromium.org627d9002016-04-29 00:00:52 +00004781 stdout = RunCommand(cmd + [gn_diff_file],
4782 shell=sys.platform == 'win32',
4783 cwd=top_dir)
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004784 if opts.diff:
4785 sys.stdout.write(stdout)
4786
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004787 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004788
4789
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004790@subcommand.usage('<codereview url or issue id>')
4791def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004792 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004793 _, args = parser.parse_args(args)
4794
4795 if len(args) != 1:
4796 parser.print_help()
4797 return 1
4798
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004799 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00004800 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004801 parser.print_help()
4802 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00004803 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004804
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004805 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00004806 output = RunGit(['config', '--local', '--get-regexp',
4807 r'branch\..*\.%s' % issueprefix],
4808 error_ok=True)
4809 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004810 if issue == target_issue:
4811 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004812
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004813 branches = []
4814 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00004815 branches.extend(find_issues(cls.IssueSettingSuffix()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004816 if len(branches) == 0:
4817 print 'No branch found for issue %s.' % target_issue
4818 return 1
4819 if len(branches) == 1:
4820 RunGit(['checkout', branches[0]])
4821 else:
4822 print 'Multiple branches match issue %s:' % target_issue
4823 for i in range(len(branches)):
4824 print '%d: %s' % (i, branches[i])
4825 which = raw_input('Choose by index: ')
4826 try:
4827 RunGit(['checkout', branches[int(which)]])
4828 except (IndexError, ValueError):
4829 print 'Invalid selection, not checking out any branch.'
4830 return 1
4831
4832 return 0
4833
4834
maruel@chromium.org29404b52014-09-08 22:58:00 +00004835def CMDlol(parser, args):
4836 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00004837 print zlib.decompress(base64.b64decode(
4838 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
4839 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
4840 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
4841 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00004842 return 0
4843
4844
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004845class OptionParser(optparse.OptionParser):
4846 """Creates the option parse and add --verbose support."""
4847 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004848 optparse.OptionParser.__init__(
4849 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004850 self.add_option(
4851 '-v', '--verbose', action='count', default=0,
4852 help='Use 2 times for more debugging info')
4853
4854 def parse_args(self, args=None, values=None):
4855 options, args = optparse.OptionParser.parse_args(self, args, values)
4856 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
4857 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
4858 return options, args
4859
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004860
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004861def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00004862 if sys.hexversion < 0x02060000:
4863 print >> sys.stderr, (
4864 '\nYour python version %s is unsupported, please upgrade.\n' %
4865 sys.version.split(' ', 1)[0])
4866 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004867
maruel@chromium.orgddd59412011-11-30 14:20:38 +00004868 # Reload settings.
4869 global settings
4870 settings = Settings()
4871
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004872 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004873 dispatcher = subcommand.CommandDispatcher(__name__)
4874 try:
4875 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00004876 except auth.AuthenticationError as e:
4877 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004878 except urllib2.HTTPError, e:
4879 if e.code != 500:
4880 raise
4881 DieWithError(
4882 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
4883 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00004884 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004885
4886
4887if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004888 # These affect sys.stdout so do it outside of main() to simplify mocks in
4889 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00004890 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004891 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00004892 try:
4893 sys.exit(main(sys.argv[1:]))
4894 except KeyboardInterrupt:
4895 sys.stderr.write('interrupted\n')
4896 sys.exit(1)