blob: bf091b0001115a0fb3fd65bc2c48607e1f832eab [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00008"""A git-command for integrating reviews on Rietveld and Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000010from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000011from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000012import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000013import collections
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000014import glob
sheyang@google.com6ebaf782015-05-12 19:17:54 +000015import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000016import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import logging
18import optparse
19import os
tandrii@chromium.org04ea8462016-04-25 19:51:21 +000020import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000022import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000023import sys
tandrii@chromium.org04ea8462016-04-25 19:51:21 +000024import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025import textwrap
sheyang@google.com6ebaf782015-05-12 19:17:54 +000026import time
27import traceback
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000028import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000030import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000031import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000032import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000033import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000034
35try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000036 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000037except ImportError:
38 pass
39
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000040from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000041from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000043import auth
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +000044from luci_hacks import trigger_luci_job as luci_trigger
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000045import clang_format
tandrii@chromium.org71184c02016-01-13 15:18:44 +000046import commit_queue
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000047import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000048import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000049import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000050import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000051import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000052import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000053import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000054import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000055import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000056import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000057import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000058import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000059import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000060import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000061import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000062import watchlists
63
maruel@chromium.org0633fb42013-08-16 20:06:14 +000064__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000065
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000066DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000067POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000069GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
rmistry@google.comc68112d2015-03-03 12:48:06 +000070REFS_THAT_ALIAS_TO_OTHER_REFS = {
71 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
72 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
73}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000074
thestig@chromium.org44202a22014-03-11 19:22:18 +000075# Valid extensions for files we want to lint.
76DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
77DEFAULT_LINT_IGNORE_REGEX = r"$^"
78
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000079# Shortcut since it quickly becomes redundant.
80Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000081
maruel@chromium.orgddd59412011-11-30 14:20:38 +000082# Initialized in main()
83settings = None
84
85
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000086def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000087 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000088 sys.exit(1)
89
90
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000091def GetNoGitPagerEnv():
92 env = os.environ.copy()
93 # 'cat' is a magical git string that disables pagers on all platforms.
94 env['GIT_PAGER'] = 'cat'
95 return env
96
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000097
bsep@chromium.org627d9002016-04-29 00:00:52 +000098def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000099 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000100 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000101 except subprocess2.CalledProcessError as e:
102 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000103 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000104 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000105 'Command "%s" failed.\n%s' % (
106 ' '.join(args), error_message or e.stdout or ''))
107 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000108
109
110def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000111 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000112 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000113
114
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000115def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000116 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000117 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000118 if suppress_stderr:
119 stderr = subprocess2.VOID
120 else:
121 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000122 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000123 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000124 stdout=subprocess2.PIPE,
125 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000126 return code, out[0]
127 except ValueError:
128 # When the subprocess fails, it returns None. That triggers a ValueError
129 # when trying to unpack the return value into (out, code).
130 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000131
132
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000133def RunGitSilent(args):
134 """Returns stdout, suppresses stderr and ingores the return code."""
135 return RunGitWithCode(args, suppress_stderr=True)[1]
136
137
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000138def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000139 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000140 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000141 return (version.startswith(prefix) and
142 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000143
144
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000145def BranchExists(branch):
146 """Return True if specified branch exists."""
147 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
148 suppress_stderr=True)
149 return not code
150
151
maruel@chromium.org90541732011-04-01 17:54:18 +0000152def ask_for_data(prompt):
153 try:
154 return raw_input(prompt)
155 except KeyboardInterrupt:
156 # Hide the exception.
157 sys.exit(1)
158
159
iannucci@chromium.org79540052012-10-19 23:15:26 +0000160def git_set_branch_value(key, value):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000161 branch = GetCurrentBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000162 if not branch:
163 return
164
165 cmd = ['config']
166 if isinstance(value, int):
167 cmd.append('--int')
168 git_key = 'branch.%s.%s' % (branch, key)
169 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000170
171
172def git_get_branch_default(key, default):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000173 branch = GetCurrentBranch()
iannucci@chromium.org79540052012-10-19 23:15:26 +0000174 if branch:
175 git_key = 'branch.%s.%s' % (branch, key)
176 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
177 try:
178 return int(stdout.strip())
179 except ValueError:
180 pass
181 return default
182
183
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000184def add_git_similarity(parser):
185 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000186 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000187 help='Sets the percentage that a pair of files need to match in order to'
188 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000189 parser.add_option(
190 '--find-copies', action='store_true',
191 help='Allows git to look for copies.')
192 parser.add_option(
193 '--no-find-copies', action='store_false', dest='find_copies',
194 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000195
196 old_parser_args = parser.parse_args
197 def Parse(args):
198 options, args = old_parser_args(args)
199
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000200 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000201 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000202 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000203 print('Note: Saving similarity of %d%% in git config.'
204 % options.similarity)
205 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000206
iannucci@chromium.org79540052012-10-19 23:15:26 +0000207 options.similarity = max(0, min(options.similarity, 100))
208
209 if options.find_copies is None:
210 options.find_copies = bool(
211 git_get_branch_default('git-find-copies', True))
212 else:
213 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000214
215 print('Using %d%% similarity for rename/copy detection. '
216 'Override with --similarity.' % options.similarity)
217
218 return options, args
219 parser.parse_args = Parse
220
221
machenbach@chromium.org45453142015-09-15 08:45:22 +0000222def _get_properties_from_options(options):
223 properties = dict(x.split('=', 1) for x in options.properties)
224 for key, val in properties.iteritems():
225 try:
226 properties[key] = json.loads(val)
227 except ValueError:
228 pass # If a value couldn't be evaluated, treat it as a string.
229 return properties
230
231
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000232def _prefix_master(master):
233 """Convert user-specified master name to full master name.
234
235 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
236 name, while the developers always use shortened master name
237 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
238 function does the conversion for buildbucket migration.
239 """
240 prefix = 'master.'
241 if master.startswith(prefix):
242 return master
243 return '%s%s' % (prefix, master)
244
245
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000246def _buildbucket_retry(operation_name, http, *args, **kwargs):
247 """Retries requests to buildbucket service and returns parsed json content."""
248 try_count = 0
249 while True:
250 response, content = http.request(*args, **kwargs)
251 try:
252 content_json = json.loads(content)
253 except ValueError:
254 content_json = None
255
256 # Buildbucket could return an error even if status==200.
257 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000258 error = content_json.get('error')
259 if error.get('code') == 403:
260 raise BuildbucketResponseException(
261 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000262 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000263 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000264 raise BuildbucketResponseException(msg)
265
266 if response.status == 200:
267 if not content_json:
268 raise BuildbucketResponseException(
269 'Buildbucket returns invalid json content: %s.\n'
270 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
271 content)
272 return content_json
273 if response.status < 500 or try_count >= 2:
274 raise httplib2.HttpLib2Error(content)
275
276 # status >= 500 means transient failures.
277 logging.debug('Transient errors when %s. Will retry.', operation_name)
278 time.sleep(0.5 + 1.5*try_count)
279 try_count += 1
280 assert False, 'unreachable'
281
282
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000283def trigger_luci_job(changelist, masters, options):
284 """Send a job to run on LUCI."""
285 issue_props = changelist.GetIssueProperties()
286 issue = changelist.GetIssue()
287 patchset = changelist.GetMostRecentPatchset()
288 for builders_and_tests in sorted(masters.itervalues()):
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000289 # TODO(hinoka et al): add support for other properties.
290 # Currently, this completely ignores testfilter and other properties.
291 for builder in sorted(builders_and_tests):
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000292 luci_trigger.trigger(
293 builder, 'HEAD', issue, patchset, issue_props['project'])
294
295
machenbach@chromium.org45453142015-09-15 08:45:22 +0000296def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000297 rietveld_url = settings.GetDefaultServerUrl()
298 rietveld_host = urlparse.urlparse(rietveld_url).hostname
299 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
300 http = authenticator.authorize(httplib2.Http())
301 http.force_exception_to_status_code = True
302 issue_props = changelist.GetIssueProperties()
303 issue = changelist.GetIssue()
304 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000305 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000306
307 buildbucket_put_url = (
308 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000309 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000310 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
311 hostname=rietveld_host,
312 issue=issue,
313 patch=patchset)
314
315 batch_req_body = {'builds': []}
316 print_text = []
317 print_text.append('Tried jobs on:')
318 for master, builders_and_tests in sorted(masters.iteritems()):
319 print_text.append('Master: %s' % master)
320 bucket = _prefix_master(master)
321 for builder, tests in sorted(builders_and_tests.iteritems()):
322 print_text.append(' %s: %s' % (builder, tests))
323 parameters = {
324 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000325 'changes': [{
326 'author': {'email': issue_props['owner_email']},
327 'revision': options.revision,
328 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000329 'properties': {
330 'category': category,
331 'issue': issue,
332 'master': master,
333 'patch_project': issue_props['project'],
334 'patch_storage': 'rietveld',
335 'patchset': patchset,
336 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000337 'rietveld': rietveld_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000338 },
339 }
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000340 if tests:
341 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000342 if properties:
343 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000344 if options.clobber:
345 parameters['properties']['clobber'] = True
346 batch_req_body['builds'].append(
347 {
348 'bucket': bucket,
349 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000350 'client_operation_id': str(uuid.uuid4()),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000351 'tags': ['builder:%s' % builder,
352 'buildset:%s' % buildset,
353 'master:%s' % master,
354 'user_agent:git_cl_try']
355 }
356 )
357
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000358 _buildbucket_retry(
359 'triggering tryjobs',
360 http,
361 buildbucket_put_url,
362 'PUT',
363 body=json.dumps(batch_req_body),
364 headers={'Content-Type': 'application/json'}
365 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000366 print_text.append('To see results here, run: git cl try-results')
367 print_text.append('To see results in browser, run: git cl web')
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000368 print '\n'.join(print_text)
kjellander@chromium.org44424542015-06-02 18:35:29 +0000369
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000370
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000371def fetch_try_jobs(auth_config, changelist, options):
372 """Fetches tryjobs from buildbucket.
373
374 Returns a map from build id to build info as json dictionary.
375 """
376 rietveld_url = settings.GetDefaultServerUrl()
377 rietveld_host = urlparse.urlparse(rietveld_url).hostname
378 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
379 if authenticator.has_cached_credentials():
380 http = authenticator.authorize(httplib2.Http())
381 else:
382 print ('Warning: Some results might be missing because %s' %
383 # Get the message on how to login.
384 auth.LoginRequiredError(rietveld_host).message)
385 http = httplib2.Http()
386
387 http.force_exception_to_status_code = True
388
389 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
390 hostname=rietveld_host,
391 issue=changelist.GetIssue(),
392 patch=options.patchset)
393 params = {'tag': 'buildset:%s' % buildset}
394
395 builds = {}
396 while True:
397 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
398 hostname=options.buildbucket_host,
399 params=urllib.urlencode(params))
400 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
401 for build in content.get('builds', []):
402 builds[build['id']] = build
403 if 'next_cursor' in content:
404 params['start_cursor'] = content['next_cursor']
405 else:
406 break
407 return builds
408
409
410def print_tryjobs(options, builds):
411 """Prints nicely result of fetch_try_jobs."""
412 if not builds:
413 print 'No tryjobs scheduled'
414 return
415
416 # Make a copy, because we'll be modifying builds dictionary.
417 builds = builds.copy()
418 builder_names_cache = {}
419
420 def get_builder(b):
421 try:
422 return builder_names_cache[b['id']]
423 except KeyError:
424 try:
425 parameters = json.loads(b['parameters_json'])
426 name = parameters['builder_name']
427 except (ValueError, KeyError) as error:
428 print 'WARNING: failed to get builder name for build %s: %s' % (
429 b['id'], error)
430 name = None
431 builder_names_cache[b['id']] = name
432 return name
433
434 def get_bucket(b):
435 bucket = b['bucket']
436 if bucket.startswith('master.'):
437 return bucket[len('master.'):]
438 return bucket
439
440 if options.print_master:
441 name_fmt = '%%-%ds %%-%ds' % (
442 max(len(str(get_bucket(b))) for b in builds.itervalues()),
443 max(len(str(get_builder(b))) for b in builds.itervalues()))
444 def get_name(b):
445 return name_fmt % (get_bucket(b), get_builder(b))
446 else:
447 name_fmt = '%%-%ds' % (
448 max(len(str(get_builder(b))) for b in builds.itervalues()))
449 def get_name(b):
450 return name_fmt % get_builder(b)
451
452 def sort_key(b):
453 return b['status'], b.get('result'), get_name(b), b.get('url')
454
455 def pop(title, f, color=None, **kwargs):
456 """Pop matching builds from `builds` dict and print them."""
457
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000458 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000459 colorize = str
460 else:
461 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
462
463 result = []
464 for b in builds.values():
465 if all(b.get(k) == v for k, v in kwargs.iteritems()):
466 builds.pop(b['id'])
467 result.append(b)
468 if result:
469 print colorize(title)
470 for b in sorted(result, key=sort_key):
471 print ' ', colorize('\t'.join(map(str, f(b))))
472
473 total = len(builds)
474 pop(status='COMPLETED', result='SUCCESS',
475 title='Successes:', color=Fore.GREEN,
476 f=lambda b: (get_name(b), b.get('url')))
477 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
478 title='Infra Failures:', color=Fore.MAGENTA,
479 f=lambda b: (get_name(b), b.get('url')))
480 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
481 title='Failures:', color=Fore.RED,
482 f=lambda b: (get_name(b), b.get('url')))
483 pop(status='COMPLETED', result='CANCELED',
484 title='Canceled:', color=Fore.MAGENTA,
485 f=lambda b: (get_name(b),))
486 pop(status='COMPLETED', result='FAILURE',
487 failure_reason='INVALID_BUILD_DEFINITION',
488 title='Wrong master/builder name:', color=Fore.MAGENTA,
489 f=lambda b: (get_name(b),))
490 pop(status='COMPLETED', result='FAILURE',
491 title='Other failures:',
492 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
493 pop(status='COMPLETED',
494 title='Other finished:',
495 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
496 pop(status='STARTED',
497 title='Started:', color=Fore.YELLOW,
498 f=lambda b: (get_name(b), b.get('url')))
499 pop(status='SCHEDULED',
500 title='Scheduled:',
501 f=lambda b: (get_name(b), 'id=%s' % b['id']))
502 # The last section is just in case buildbucket API changes OR there is a bug.
503 pop(title='Other:',
504 f=lambda b: (get_name(b), 'id=%s' % b['id']))
505 assert len(builds) == 0
506 print 'Total: %d tryjobs' % total
507
508
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000509def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
510 """Return the corresponding git ref if |base_url| together with |glob_spec|
511 matches the full |url|.
512
513 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
514 """
515 fetch_suburl, as_ref = glob_spec.split(':')
516 if allow_wildcards:
517 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
518 if glob_match:
519 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
520 # "branches/{472,597,648}/src:refs/remotes/svn/*".
521 branch_re = re.escape(base_url)
522 if glob_match.group(1):
523 branch_re += '/' + re.escape(glob_match.group(1))
524 wildcard = glob_match.group(2)
525 if wildcard == '*':
526 branch_re += '([^/]*)'
527 else:
528 # Escape and replace surrounding braces with parentheses and commas
529 # with pipe symbols.
530 wildcard = re.escape(wildcard)
531 wildcard = re.sub('^\\\\{', '(', wildcard)
532 wildcard = re.sub('\\\\,', '|', wildcard)
533 wildcard = re.sub('\\\\}$', ')', wildcard)
534 branch_re += wildcard
535 if glob_match.group(3):
536 branch_re += re.escape(glob_match.group(3))
537 match = re.match(branch_re, url)
538 if match:
539 return re.sub('\*$', match.group(1), as_ref)
540
541 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
542 if fetch_suburl:
543 full_url = base_url + '/' + fetch_suburl
544 else:
545 full_url = base_url
546 if full_url == url:
547 return as_ref
548 return None
549
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000550
iannucci@chromium.org79540052012-10-19 23:15:26 +0000551def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000552 """Prints statistics about the change to the user."""
553 # --no-ext-diff is broken in some versions of Git, so try to work around
554 # this by overriding the environment (but there is still a problem if the
555 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000556 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000557 if 'GIT_EXTERNAL_DIFF' in env:
558 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000559
560 if find_copies:
561 similarity_options = ['--find-copies-harder', '-l100000',
562 '-C%s' % similarity]
563 else:
564 similarity_options = ['-M%s' % similarity]
565
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000566 try:
567 stdout = sys.stdout.fileno()
568 except AttributeError:
569 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000570 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000571 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000572 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000573 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000574
575
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000576class BuildbucketResponseException(Exception):
577 pass
578
579
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000580class Settings(object):
581 def __init__(self):
582 self.default_server = None
583 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000584 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000585 self.is_git_svn = None
586 self.svn_branch = None
587 self.tree_status_url = None
588 self.viewvc_url = None
589 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000590 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000591 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000592 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000593 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000594 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000595 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000596 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000597
598 def LazyUpdateIfNeeded(self):
599 """Updates the settings from a codereview.settings file, if available."""
600 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000601 # The only value that actually changes the behavior is
602 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000603 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000604 error_ok=True
605 ).strip().lower()
606
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000607 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000608 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000609 LoadCodereviewSettingsFromFile(cr_settings_file)
610 self.updated = True
611
612 def GetDefaultServerUrl(self, error_ok=False):
613 if not self.default_server:
614 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000615 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000616 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000617 if error_ok:
618 return self.default_server
619 if not self.default_server:
620 error_message = ('Could not find settings file. You must configure '
621 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000622 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000623 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000624 return self.default_server
625
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000626 @staticmethod
627 def GetRelativeRoot():
628 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000629
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000630 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000631 if self.root is None:
632 self.root = os.path.abspath(self.GetRelativeRoot())
633 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000634
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000635 def GetGitMirror(self, remote='origin'):
636 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000637 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000638 if not os.path.isdir(local_url):
639 return None
640 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
641 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
642 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
643 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
644 if mirror.exists():
645 return mirror
646 return None
647
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000648 def GetIsGitSvn(self):
649 """Return true if this repo looks like it's using git-svn."""
650 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000651 if self.GetPendingRefPrefix():
652 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
653 self.is_git_svn = False
654 else:
655 # If you have any "svn-remote.*" config keys, we think you're using svn.
656 self.is_git_svn = RunGitWithCode(
657 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000658 return self.is_git_svn
659
660 def GetSVNBranch(self):
661 if self.svn_branch is None:
662 if not self.GetIsGitSvn():
663 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
664
665 # Try to figure out which remote branch we're based on.
666 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000667 # 1) iterate through our branch history and find the svn URL.
668 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000669
670 # regexp matching the git-svn line that contains the URL.
671 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
672
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000673 # We don't want to go through all of history, so read a line from the
674 # pipe at a time.
675 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000676 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000677 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
678 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000679 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000680 for line in proc.stdout:
681 match = git_svn_re.match(line)
682 if match:
683 url = match.group(1)
684 proc.stdout.close() # Cut pipe.
685 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000686
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000687 if url:
688 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
689 remotes = RunGit(['config', '--get-regexp',
690 r'^svn-remote\..*\.url']).splitlines()
691 for remote in remotes:
692 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000693 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000694 remote = match.group(1)
695 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000696 rewrite_root = RunGit(
697 ['config', 'svn-remote.%s.rewriteRoot' % remote],
698 error_ok=True).strip()
699 if rewrite_root:
700 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000701 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000702 ['config', 'svn-remote.%s.fetch' % remote],
703 error_ok=True).strip()
704 if fetch_spec:
705 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
706 if self.svn_branch:
707 break
708 branch_spec = RunGit(
709 ['config', 'svn-remote.%s.branches' % remote],
710 error_ok=True).strip()
711 if branch_spec:
712 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
713 if self.svn_branch:
714 break
715 tag_spec = RunGit(
716 ['config', 'svn-remote.%s.tags' % remote],
717 error_ok=True).strip()
718 if tag_spec:
719 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
720 if self.svn_branch:
721 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000722
723 if not self.svn_branch:
724 DieWithError('Can\'t guess svn branch -- try specifying it on the '
725 'command line')
726
727 return self.svn_branch
728
729 def GetTreeStatusUrl(self, error_ok=False):
730 if not self.tree_status_url:
731 error_message = ('You must configure your tree status URL by running '
732 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000733 self.tree_status_url = self._GetRietveldConfig(
734 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000735 return self.tree_status_url
736
737 def GetViewVCUrl(self):
738 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000739 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000740 return self.viewvc_url
741
rmistry@google.com90752582014-01-14 21:04:50 +0000742 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000743 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000744
rmistry@google.com78948ed2015-07-08 23:09:57 +0000745 def GetIsSkipDependencyUpload(self, branch_name):
746 """Returns true if specified branch should skip dep uploads."""
747 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
748 error_ok=True)
749
rmistry@google.com5626a922015-02-26 14:03:30 +0000750 def GetRunPostUploadHook(self):
751 run_post_upload_hook = self._GetRietveldConfig(
752 'run-post-upload-hook', error_ok=True)
753 return run_post_upload_hook == "True"
754
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000755 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000756 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000757
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000758 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000759 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000760
ukai@chromium.orge8077812012-02-03 03:41:46 +0000761 def GetIsGerrit(self):
762 """Return true if this repo is assosiated with gerrit code review system."""
763 if self.is_gerrit is None:
764 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
765 return self.is_gerrit
766
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000767 def GetSquashGerritUploads(self):
768 """Return true if uploads to Gerrit should be squashed by default."""
769 if self.squash_gerrit_uploads is None:
770 self.squash_gerrit_uploads = (
771 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
772 error_ok=True).strip() == 'true')
773 return self.squash_gerrit_uploads
774
tandrii@chromium.org28253532016-04-14 13:46:56 +0000775 def GetGerritSkipEnsureAuthenticated(self):
776 """Return True if EnsureAuthenticated should not be done for Gerrit
777 uploads."""
778 if self.gerrit_skip_ensure_authenticated is None:
779 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000780 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000781 error_ok=True).strip() == 'true')
782 return self.gerrit_skip_ensure_authenticated
783
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000784 def GetGitEditor(self):
785 """Return the editor specified in the git config, or None if none is."""
786 if self.git_editor is None:
787 self.git_editor = self._GetConfig('core.editor', error_ok=True)
788 return self.git_editor or None
789
thestig@chromium.org44202a22014-03-11 19:22:18 +0000790 def GetLintRegex(self):
791 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
792 DEFAULT_LINT_REGEX)
793
794 def GetLintIgnoreRegex(self):
795 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
796 DEFAULT_LINT_IGNORE_REGEX)
797
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000798 def GetProject(self):
799 if not self.project:
800 self.project = self._GetRietveldConfig('project', error_ok=True)
801 return self.project
802
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000803 def GetForceHttpsCommitUrl(self):
804 if not self.force_https_commit_url:
805 self.force_https_commit_url = self._GetRietveldConfig(
806 'force-https-commit-url', error_ok=True)
807 return self.force_https_commit_url
808
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000809 def GetPendingRefPrefix(self):
810 if not self.pending_ref_prefix:
811 self.pending_ref_prefix = self._GetRietveldConfig(
812 'pending-ref-prefix', error_ok=True)
813 return self.pending_ref_prefix
814
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000815 def _GetRietveldConfig(self, param, **kwargs):
816 return self._GetConfig('rietveld.' + param, **kwargs)
817
rmistry@google.com78948ed2015-07-08 23:09:57 +0000818 def _GetBranchConfig(self, branch_name, param, **kwargs):
819 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
820
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000821 def _GetConfig(self, param, **kwargs):
822 self.LazyUpdateIfNeeded()
823 return RunGit(['config', param], **kwargs).strip()
824
825
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000826def ShortBranchName(branch):
827 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000828 return branch.replace('refs/heads/', '', 1)
829
830
831def GetCurrentBranchRef():
832 """Returns branch ref (e.g., refs/heads/master) or None."""
833 return RunGit(['symbolic-ref', 'HEAD'],
834 stderr=subprocess2.VOID, error_ok=True).strip() or None
835
836
837def GetCurrentBranch():
838 """Returns current branch or None.
839
840 For refs/heads/* branches, returns just last part. For others, full ref.
841 """
842 branchref = GetCurrentBranchRef()
843 if branchref:
844 return ShortBranchName(branchref)
845 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000846
847
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000848class _CQState(object):
849 """Enum for states of CL with respect to Commit Queue."""
850 NONE = 'none'
851 DRY_RUN = 'dry_run'
852 COMMIT = 'commit'
853
854 ALL_STATES = [NONE, DRY_RUN, COMMIT]
855
856
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000857class _ParsedIssueNumberArgument(object):
858 def __init__(self, issue=None, patchset=None, hostname=None):
859 self.issue = issue
860 self.patchset = patchset
861 self.hostname = hostname
862
863 @property
864 def valid(self):
865 return self.issue is not None
866
867
868class _RietveldParsedIssueNumberArgument(_ParsedIssueNumberArgument):
869 def __init__(self, *args, **kwargs):
870 self.patch_url = kwargs.pop('patch_url', None)
871 super(_RietveldParsedIssueNumberArgument, self).__init__(*args, **kwargs)
872
873
874def ParseIssueNumberArgument(arg):
875 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
876 fail_result = _ParsedIssueNumberArgument()
877
878 if arg.isdigit():
879 return _ParsedIssueNumberArgument(issue=int(arg))
880 if not arg.startswith('http'):
881 return fail_result
882 url = gclient_utils.UpgradeToHttps(arg)
883 try:
884 parsed_url = urlparse.urlparse(url)
885 except ValueError:
886 return fail_result
887 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
888 tmp = cls.ParseIssueURL(parsed_url)
889 if tmp is not None:
890 return tmp
891 return fail_result
892
893
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000894class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000895 """Changelist works with one changelist in local branch.
896
897 Supports two codereview backends: Rietveld or Gerrit, selected at object
898 creation.
899
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +0000900 Notes:
901 * Not safe for concurrent multi-{thread,process} use.
902 * Caches values from current branch. Therefore, re-use after branch change
903 with care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000904 """
905
906 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
907 """Create a new ChangeList instance.
908
909 If issue is given, the codereview must be given too.
910
911 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
912 Otherwise, it's decided based on current configuration of the local branch,
913 with default being 'rietveld' for backwards compatibility.
914 See _load_codereview_impl for more details.
915
916 **kwargs will be passed directly to codereview implementation.
917 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000918 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000919 global settings
920 if not settings:
921 # Happens when git_cl.py is used as a utility library.
922 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000923
924 if issue:
925 assert codereview, 'codereview must be known, if issue is known'
926
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000927 self.branchref = branchref
928 if self.branchref:
929 self.branch = ShortBranchName(self.branchref)
930 else:
931 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000932 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000933 self.lookedup_issue = False
934 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000935 self.has_description = False
936 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000937 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000938 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000939 self.cc = None
940 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000941 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000942
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000943 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000944 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000945 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000946 assert self._codereview_impl
947 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000948
949 def _load_codereview_impl(self, codereview=None, **kwargs):
950 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000951 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
952 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
953 self._codereview = codereview
954 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000955 return
956
957 # Automatic selection based on issue number set for a current branch.
958 # Rietveld takes precedence over Gerrit.
959 assert not self.issue
960 # Whether we find issue or not, we are doing the lookup.
961 self.lookedup_issue = True
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000962 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000963 setting = cls.IssueSetting(self.GetBranch())
964 issue = RunGit(['config', setting], error_ok=True).strip()
965 if issue:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000966 self._codereview = codereview
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000967 self._codereview_impl = cls(self, **kwargs)
968 self.issue = int(issue)
969 return
970
971 # No issue is set for this branch, so decide based on repo-wide settings.
972 return self._load_codereview_impl(
973 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
974 **kwargs)
975
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000976 def IsGerrit(self):
977 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000978
979 def GetCCList(self):
980 """Return the users cc'd on this CL.
981
982 Return is a string suitable for passing to gcl with the --cc flag.
983 """
984 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000985 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000986 more_cc = ','.join(self.watchers)
987 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
988 return self.cc
989
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000990 def GetCCListWithoutDefault(self):
991 """Return the users cc'd on this CL excluding default ones."""
992 if self.cc is None:
993 self.cc = ','.join(self.watchers)
994 return self.cc
995
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000996 def SetWatchers(self, watchers):
997 """Set the list of email addresses that should be cc'd based on the changed
998 files in this CL.
999 """
1000 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001001
1002 def GetBranch(self):
1003 """Returns the short branch name, e.g. 'master'."""
1004 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001005 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001006 if not branchref:
1007 return None
1008 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001009 self.branch = ShortBranchName(self.branchref)
1010 return self.branch
1011
1012 def GetBranchRef(self):
1013 """Returns the full branch name, e.g. 'refs/heads/master'."""
1014 self.GetBranch() # Poke the lazy loader.
1015 return self.branchref
1016
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001017 def ClearBranch(self):
1018 """Clears cached branch data of this object."""
1019 self.branch = self.branchref = None
1020
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001021 @staticmethod
1022 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001023 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001024 e.g. 'origin', 'refs/heads/master'
1025 """
1026 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001027 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
1028 error_ok=True).strip()
1029 if upstream_branch:
1030 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
1031 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001032 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1033 error_ok=True).strip()
1034 if upstream_branch:
1035 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001036 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001037 # Fall back on trying a git-svn upstream branch.
1038 if settings.GetIsGitSvn():
1039 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001040 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001041 # Else, try to guess the origin remote.
1042 remote_branches = RunGit(['branch', '-r']).split()
1043 if 'origin/master' in remote_branches:
1044 # Fall back on origin/master if it exits.
1045 remote = 'origin'
1046 upstream_branch = 'refs/heads/master'
1047 elif 'origin/trunk' in remote_branches:
1048 # Fall back on origin/trunk if it exists. Generally a shared
1049 # git-svn clone
1050 remote = 'origin'
1051 upstream_branch = 'refs/heads/trunk'
1052 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001053 DieWithError(
1054 'Unable to determine default branch to diff against.\n'
1055 'Either pass complete "git diff"-style arguments, like\n'
1056 ' git cl upload origin/master\n'
1057 'or verify this branch is set up to track another \n'
1058 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001059
1060 return remote, upstream_branch
1061
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001062 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001063 upstream_branch = self.GetUpstreamBranch()
1064 if not BranchExists(upstream_branch):
1065 DieWithError('The upstream for the current branch (%s) does not exist '
1066 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001067 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001068 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001069
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001070 def GetUpstreamBranch(self):
1071 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001072 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001073 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001074 upstream_branch = upstream_branch.replace('refs/heads/',
1075 'refs/remotes/%s/' % remote)
1076 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1077 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001078 self.upstream_branch = upstream_branch
1079 return self.upstream_branch
1080
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001081 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001082 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001083 remote, branch = None, self.GetBranch()
1084 seen_branches = set()
1085 while branch not in seen_branches:
1086 seen_branches.add(branch)
1087 remote, branch = self.FetchUpstreamTuple(branch)
1088 branch = ShortBranchName(branch)
1089 if remote != '.' or branch.startswith('refs/remotes'):
1090 break
1091 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001092 remotes = RunGit(['remote'], error_ok=True).split()
1093 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001094 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001095 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001096 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001097 logging.warning('Could not determine which remote this change is '
1098 'associated with, so defaulting to "%s". This may '
1099 'not be what you want. You may prevent this message '
1100 'by running "git svn info" as documented here: %s',
1101 self._remote,
1102 GIT_INSTRUCTIONS_URL)
1103 else:
1104 logging.warn('Could not determine which remote this change is '
1105 'associated with. You may prevent this message by '
1106 'running "git svn info" as documented here: %s',
1107 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001108 branch = 'HEAD'
1109 if branch.startswith('refs/remotes'):
1110 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001111 elif branch.startswith('refs/branch-heads/'):
1112 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001113 else:
1114 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001115 return self._remote
1116
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001117 def GitSanityChecks(self, upstream_git_obj):
1118 """Checks git repo status and ensures diff is from local commits."""
1119
sbc@chromium.org79706062015-01-14 21:18:12 +00001120 if upstream_git_obj is None:
1121 if self.GetBranch() is None:
1122 print >> sys.stderr, (
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00001123 'ERROR: unable to determine current branch (detached HEAD?)')
sbc@chromium.org79706062015-01-14 21:18:12 +00001124 else:
1125 print >> sys.stderr, (
1126 'ERROR: no upstream branch')
1127 return False
1128
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001129 # Verify the commit we're diffing against is in our current branch.
1130 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1131 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1132 if upstream_sha != common_ancestor:
1133 print >> sys.stderr, (
1134 'ERROR: %s is not in the current branch. You may need to rebase '
1135 'your tracking branch' % upstream_sha)
1136 return False
1137
1138 # List the commits inside the diff, and verify they are all local.
1139 commits_in_diff = RunGit(
1140 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1141 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1142 remote_branch = remote_branch.strip()
1143 if code != 0:
1144 _, remote_branch = self.GetRemoteBranch()
1145
1146 commits_in_remote = RunGit(
1147 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1148
1149 common_commits = set(commits_in_diff) & set(commits_in_remote)
1150 if common_commits:
1151 print >> sys.stderr, (
1152 'ERROR: Your diff contains %d commits already in %s.\n'
1153 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1154 'the diff. If you are using a custom git flow, you can override'
1155 ' the reference used for this check with "git config '
1156 'gitcl.remotebranch <git-ref>".' % (
1157 len(common_commits), remote_branch, upstream_git_obj))
1158 return False
1159 return True
1160
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001161 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001162 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001163
1164 Returns None if it is not set.
1165 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001166 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
1167 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001168
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001169 def GetGitSvnRemoteUrl(self):
1170 """Return the configured git-svn remote URL parsed from git svn info.
1171
1172 Returns None if it is not set.
1173 """
1174 # URL is dependent on the current directory.
1175 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1176 if data:
1177 keys = dict(line.split(': ', 1) for line in data.splitlines()
1178 if ': ' in line)
1179 return keys.get('URL', None)
1180 return None
1181
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001182 def GetRemoteUrl(self):
1183 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1184
1185 Returns None if there is no remote.
1186 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001187 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001188 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1189
1190 # If URL is pointing to a local directory, it is probably a git cache.
1191 if os.path.isdir(url):
1192 url = RunGit(['config', 'remote.%s.url' % remote],
1193 error_ok=True,
1194 cwd=url).strip()
1195 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001196
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001197 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001198 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001199 if self.issue is None and not self.lookedup_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001200 issue = RunGit(['config',
1201 self._codereview_impl.IssueSetting(self.GetBranch())],
1202 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001203 self.issue = int(issue) or None if issue else None
1204 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001205 return self.issue
1206
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001207 def GetIssueURL(self):
1208 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001209 issue = self.GetIssue()
1210 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001211 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001212 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001213
1214 def GetDescription(self, pretty=False):
1215 if not self.has_description:
1216 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001217 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001218 self.has_description = True
1219 if pretty:
1220 wrapper = textwrap.TextWrapper()
1221 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1222 return wrapper.fill(self.description)
1223 return self.description
1224
1225 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001226 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001227 if self.patchset is None and not self.lookedup_patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001228 patchset = RunGit(['config', self._codereview_impl.PatchsetSetting()],
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001229 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001230 self.patchset = int(patchset) or None if patchset else None
1231 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001232 return self.patchset
1233
1234 def SetPatchset(self, patchset):
1235 """Set this branch's patchset. If patchset=0, clears the patchset."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001236 patchset_setting = self._codereview_impl.PatchsetSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001237 if patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001238 RunGit(['config', patchset_setting, str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001239 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001240 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001241 RunGit(['config', '--unset', patchset_setting],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001242 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001243 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001244
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001245 def SetIssue(self, issue=None):
1246 """Set this branch's issue. If issue isn't given, clears the issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001247 issue_setting = self._codereview_impl.IssueSetting(self.GetBranch())
1248 codereview_setting = self._codereview_impl.GetCodereviewServerSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001249 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001250 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001251 RunGit(['config', issue_setting, str(issue)])
1252 codereview_server = self._codereview_impl.GetCodereviewServer()
1253 if codereview_server:
1254 RunGit(['config', codereview_setting, codereview_server])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001255 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +00001256 current_issue = self.GetIssue()
1257 if current_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001258 RunGit(['config', '--unset', issue_setting])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001259 self.issue = None
1260 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001261
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001262 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001263 if not self.GitSanityChecks(upstream_branch):
1264 DieWithError('\nGit sanity check failure')
1265
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001266 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001267 if not root:
1268 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001269 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001270
1271 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001272 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001273 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001274 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001275 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001276 except subprocess2.CalledProcessError:
1277 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001278 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001279 'This branch probably doesn\'t exist anymore. To reset the\n'
1280 'tracking branch, please run\n'
1281 ' git branch --set-upstream %s trunk\n'
1282 'replacing trunk with origin/master or the relevant branch') %
1283 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001284
maruel@chromium.org52424302012-08-29 15:14:30 +00001285 issue = self.GetIssue()
1286 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001287 if issue:
1288 description = self.GetDescription()
1289 else:
1290 # If the change was never uploaded, use the log messages of all commits
1291 # up to the branch point, as git cl upload will prefill the description
1292 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001293 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1294 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001295
1296 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001297 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001298 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001299 name,
1300 description,
1301 absroot,
1302 files,
1303 issue,
1304 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001305 author,
1306 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001307
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001308 def UpdateDescription(self, description):
1309 self.description = description
1310 return self._codereview_impl.UpdateDescriptionRemote(description)
1311
1312 def RunHook(self, committing, may_prompt, verbose, change):
1313 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1314 try:
1315 return presubmit_support.DoPresubmitChecks(change, committing,
1316 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1317 default_presubmit=None, may_prompt=may_prompt,
machenbach@chromium.org6996a102016-04-29 10:32:47 +00001318 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit())
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001319 except presubmit_support.PresubmitFailure, e:
1320 DieWithError(
1321 ('%s\nMaybe your depot_tools is out of date?\n'
1322 'If all fails, contact maruel@') % e)
1323
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001324 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1325 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001326 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1327 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001328 else:
1329 # Assume url.
1330 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1331 urlparse.urlparse(issue_arg))
1332 if not parsed_issue_arg or not parsed_issue_arg.valid:
1333 DieWithError('Failed to parse issue argument "%s". '
1334 'Must be an issue number or a valid URL.' % issue_arg)
1335 return self._codereview_impl.CMDPatchWithParsedIssue(
1336 parsed_issue_arg, reject, nocommit, directory)
1337
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001338 def CMDUpload(self, options, git_diff_args, orig_args):
1339 """Uploads a change to codereview."""
1340 if git_diff_args:
1341 # TODO(ukai): is it ok for gerrit case?
1342 base_branch = git_diff_args[0]
1343 else:
1344 if self.GetBranch() is None:
1345 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1346
1347 # Default to diffing against common ancestor of upstream branch
1348 base_branch = self.GetCommonAncestorWithUpstream()
1349 git_diff_args = [base_branch, 'HEAD']
1350
1351 # Make sure authenticated to codereview before running potentially expensive
1352 # hooks. It is a fast, best efforts check. Codereview still can reject the
1353 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001354 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001355
1356 # Apply watchlists on upload.
1357 change = self.GetChange(base_branch, None)
1358 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1359 files = [f.LocalPath() for f in change.AffectedFiles()]
1360 if not options.bypass_watchlists:
1361 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1362
1363 if not options.bypass_hooks:
1364 if options.reviewers or options.tbr_owners:
1365 # Set the reviewer list now so that presubmit checks can access it.
1366 change_description = ChangeDescription(change.FullDescriptionText())
1367 change_description.update_reviewers(options.reviewers,
1368 options.tbr_owners,
1369 change)
1370 change.SetDescriptionText(change_description.description)
1371 hook_results = self.RunHook(committing=False,
1372 may_prompt=not options.force,
1373 verbose=options.verbose,
1374 change=change)
1375 if not hook_results.should_continue():
1376 return 1
1377 if not options.reviewers and hook_results.reviewers:
1378 options.reviewers = hook_results.reviewers.split(',')
1379
1380 if self.GetIssue():
1381 latest_patchset = self.GetMostRecentPatchset()
1382 local_patchset = self.GetPatchset()
1383 if (latest_patchset and local_patchset and
1384 local_patchset != latest_patchset):
1385 print ('The last upload made from this repository was patchset #%d but '
1386 'the most recent patchset on the server is #%d.'
1387 % (local_patchset, latest_patchset))
1388 print ('Uploading will still work, but if you\'ve uploaded to this '
1389 'issue from another machine or branch the patch you\'re '
1390 'uploading now might not include those changes.')
1391 ask_for_data('About to upload; enter to confirm.')
1392
1393 print_stats(options.similarity, options.find_copies, git_diff_args)
1394 ret = self.CMDUploadChange(options, git_diff_args, change)
1395 if not ret:
1396 git_set_branch_value('last-upload-hash',
1397 RunGit(['rev-parse', 'HEAD']).strip())
1398 # Run post upload hooks, if specified.
1399 if settings.GetRunPostUploadHook():
1400 presubmit_support.DoPostUploadExecuter(
1401 change,
1402 self,
1403 settings.GetRoot(),
1404 options.verbose,
1405 sys.stdout)
1406
1407 # Upload all dependencies if specified.
1408 if options.dependencies:
1409 print
1410 print '--dependencies has been specified.'
1411 print 'All dependent local branches will be re-uploaded.'
1412 print
1413 # Remove the dependencies flag from args so that we do not end up in a
1414 # loop.
1415 orig_args.remove('--dependencies')
1416 ret = upload_branch_deps(self, orig_args)
1417 return ret
1418
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001419 def SetCQState(self, new_state):
1420 """Update the CQ state for latest patchset.
1421
1422 Issue must have been already uploaded and known.
1423 """
1424 assert new_state in _CQState.ALL_STATES
1425 assert self.GetIssue()
1426 return self._codereview_impl.SetCQState(new_state)
1427
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001428 # Forward methods to codereview specific implementation.
1429
1430 def CloseIssue(self):
1431 return self._codereview_impl.CloseIssue()
1432
1433 def GetStatus(self):
1434 return self._codereview_impl.GetStatus()
1435
1436 def GetCodereviewServer(self):
1437 return self._codereview_impl.GetCodereviewServer()
1438
1439 def GetApprovingReviewers(self):
1440 return self._codereview_impl.GetApprovingReviewers()
1441
1442 def GetMostRecentPatchset(self):
1443 return self._codereview_impl.GetMostRecentPatchset()
1444
1445 def __getattr__(self, attr):
1446 # This is because lots of untested code accesses Rietveld-specific stuff
1447 # directly, and it's hard to fix for sure. So, just let it work, and fix
1448 # on a cases by case basis.
1449 return getattr(self._codereview_impl, attr)
1450
1451
1452class _ChangelistCodereviewBase(object):
1453 """Abstract base class encapsulating codereview specifics of a changelist."""
1454 def __init__(self, changelist):
1455 self._changelist = changelist # instance of Changelist
1456
1457 def __getattr__(self, attr):
1458 # Forward methods to changelist.
1459 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1460 # _RietveldChangelistImpl to avoid this hack?
1461 return getattr(self._changelist, attr)
1462
1463 def GetStatus(self):
1464 """Apply a rough heuristic to give a simple summary of an issue's review
1465 or CQ status, assuming adherence to a common workflow.
1466
1467 Returns None if no issue for this branch, or specific string keywords.
1468 """
1469 raise NotImplementedError()
1470
1471 def GetCodereviewServer(self):
1472 """Returns server URL without end slash, like "https://codereview.com"."""
1473 raise NotImplementedError()
1474
1475 def FetchDescription(self):
1476 """Fetches and returns description from the codereview server."""
1477 raise NotImplementedError()
1478
1479 def GetCodereviewServerSetting(self):
1480 """Returns git config setting for the codereview server."""
1481 raise NotImplementedError()
1482
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001483 @classmethod
1484 def IssueSetting(cls, branch):
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001485 return 'branch.%s.%s' % (branch, cls.IssueSettingSuffix())
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001486
1487 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001488 def IssueSettingSuffix(cls):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001489 """Returns name of git config setting which stores issue number for a given
1490 branch."""
1491 raise NotImplementedError()
1492
1493 def PatchsetSetting(self):
1494 """Returns name of git config setting which stores issue number."""
1495 raise NotImplementedError()
1496
1497 def GetRieveldObjForPresubmit(self):
1498 # This is an unfortunate Rietveld-embeddedness in presubmit.
1499 # For non-Rietveld codereviews, this probably should return a dummy object.
1500 raise NotImplementedError()
1501
1502 def UpdateDescriptionRemote(self, description):
1503 """Update the description on codereview site."""
1504 raise NotImplementedError()
1505
1506 def CloseIssue(self):
1507 """Closes the issue."""
1508 raise NotImplementedError()
1509
1510 def GetApprovingReviewers(self):
1511 """Returns a list of reviewers approving the change.
1512
1513 Note: not necessarily committers.
1514 """
1515 raise NotImplementedError()
1516
1517 def GetMostRecentPatchset(self):
1518 """Returns the most recent patchset number from the codereview site."""
1519 raise NotImplementedError()
1520
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001521 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1522 directory):
1523 """Fetches and applies the issue.
1524
1525 Arguments:
1526 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1527 reject: if True, reject the failed patch instead of switching to 3-way
1528 merge. Rietveld only.
1529 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1530 only.
1531 directory: switch to directory before applying the patch. Rietveld only.
1532 """
1533 raise NotImplementedError()
1534
1535 @staticmethod
1536 def ParseIssueURL(parsed_url):
1537 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1538 failed."""
1539 raise NotImplementedError()
1540
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001541 def EnsureAuthenticated(self, force):
1542 """Best effort check that user is authenticated with codereview server.
1543
1544 Arguments:
1545 force: whether to skip confirmation questions.
1546 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001547 raise NotImplementedError()
1548
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001549 def CMDUploadChange(self, options, args, change):
1550 """Uploads a change to codereview."""
1551 raise NotImplementedError()
1552
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001553 def SetCQState(self, new_state):
1554 """Update the CQ state for latest patchset.
1555
1556 Issue must have been already uploaded and known.
1557 """
1558 raise NotImplementedError()
1559
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001560
1561class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1562 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1563 super(_RietveldChangelistImpl, self).__init__(changelist)
1564 assert settings, 'must be initialized in _ChangelistCodereviewBase'
1565 settings.GetDefaultServerUrl()
1566
1567 self._rietveld_server = rietveld_server
1568 self._auth_config = auth_config
1569 self._props = None
1570 self._rpc_server = None
1571
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001572 def GetCodereviewServer(self):
1573 if not self._rietveld_server:
1574 # If we're on a branch then get the server potentially associated
1575 # with that branch.
1576 if self.GetIssue():
1577 rietveld_server_setting = self.GetCodereviewServerSetting()
1578 if rietveld_server_setting:
1579 self._rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1580 ['config', rietveld_server_setting], error_ok=True).strip())
1581 if not self._rietveld_server:
1582 self._rietveld_server = settings.GetDefaultServerUrl()
1583 return self._rietveld_server
1584
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001585 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001586 """Best effort check that user is authenticated with Rietveld server."""
1587 if self._auth_config.use_oauth2:
1588 authenticator = auth.get_authenticator_for_host(
1589 self.GetCodereviewServer(), self._auth_config)
1590 if not authenticator.has_cached_credentials():
1591 raise auth.LoginRequiredError(self.GetCodereviewServer())
1592
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001593 def FetchDescription(self):
1594 issue = self.GetIssue()
1595 assert issue
1596 try:
1597 return self.RpcServer().get_description(issue).strip()
1598 except urllib2.HTTPError as e:
1599 if e.code == 404:
1600 DieWithError(
1601 ('\nWhile fetching the description for issue %d, received a '
1602 '404 (not found)\n'
1603 'error. It is likely that you deleted this '
1604 'issue on the server. If this is the\n'
1605 'case, please run\n\n'
1606 ' git cl issue 0\n\n'
1607 'to clear the association with the deleted issue. Then run '
1608 'this command again.') % issue)
1609 else:
1610 DieWithError(
1611 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1612 except urllib2.URLError as e:
1613 print >> sys.stderr, (
1614 'Warning: Failed to retrieve CL description due to network '
1615 'failure.')
1616 return ''
1617
1618 def GetMostRecentPatchset(self):
1619 return self.GetIssueProperties()['patchsets'][-1]
1620
1621 def GetPatchSetDiff(self, issue, patchset):
1622 return self.RpcServer().get(
1623 '/download/issue%s_%s.diff' % (issue, patchset))
1624
1625 def GetIssueProperties(self):
1626 if self._props is None:
1627 issue = self.GetIssue()
1628 if not issue:
1629 self._props = {}
1630 else:
1631 self._props = self.RpcServer().get_issue_properties(issue, True)
1632 return self._props
1633
1634 def GetApprovingReviewers(self):
1635 return get_approving_reviewers(self.GetIssueProperties())
1636
1637 def AddComment(self, message):
1638 return self.RpcServer().add_comment(self.GetIssue(), message)
1639
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001640 def GetStatus(self):
1641 """Apply a rough heuristic to give a simple summary of an issue's review
1642 or CQ status, assuming adherence to a common workflow.
1643
1644 Returns None if no issue for this branch, or one of the following keywords:
1645 * 'error' - error from review tool (including deleted issues)
1646 * 'unsent' - not sent for review
1647 * 'waiting' - waiting for review
1648 * 'reply' - waiting for owner to reply to review
1649 * 'lgtm' - LGTM from at least one approved reviewer
1650 * 'commit' - in the commit queue
1651 * 'closed' - closed
1652 """
1653 if not self.GetIssue():
1654 return None
1655
1656 try:
1657 props = self.GetIssueProperties()
1658 except urllib2.HTTPError:
1659 return 'error'
1660
1661 if props.get('closed'):
1662 # Issue is closed.
1663 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001664 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001665 # Issue is in the commit queue.
1666 return 'commit'
1667
1668 try:
1669 reviewers = self.GetApprovingReviewers()
1670 except urllib2.HTTPError:
1671 return 'error'
1672
1673 if reviewers:
1674 # Was LGTM'ed.
1675 return 'lgtm'
1676
1677 messages = props.get('messages') or []
1678
1679 if not messages:
1680 # No message was sent.
1681 return 'unsent'
1682 if messages[-1]['sender'] != props.get('owner_email'):
1683 # Non-LGTM reply from non-owner
1684 return 'reply'
1685 return 'waiting'
1686
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001687 def UpdateDescriptionRemote(self, description):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001688 return self.RpcServer().update_description(
1689 self.GetIssue(), self.description)
1690
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001691 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001692 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001693
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001694 def SetFlag(self, flag, value):
1695 """Patchset must match."""
1696 if not self.GetPatchset():
1697 DieWithError('The patchset needs to match. Send another patchset.')
1698 try:
1699 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001700 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001701 except urllib2.HTTPError, e:
1702 if e.code == 404:
1703 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1704 if e.code == 403:
1705 DieWithError(
1706 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1707 'match?') % (self.GetIssue(), self.GetPatchset()))
1708 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001709
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001710 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001711 """Returns an upload.RpcServer() to access this review's rietveld instance.
1712 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001713 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001714 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001715 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001716 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001717 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001718
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001719 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001720 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001721 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001722
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001723 def PatchsetSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001724 """Return the git setting that stores this change's most recent patchset."""
1725 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1726
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001727 def GetCodereviewServerSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001728 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001729 branch = self.GetBranch()
1730 if branch:
1731 return 'branch.%s.rietveldserver' % branch
1732 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001733
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001734 def GetRieveldObjForPresubmit(self):
1735 return self.RpcServer()
1736
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001737 def SetCQState(self, new_state):
1738 props = self.GetIssueProperties()
1739 if props.get('private'):
1740 DieWithError('Cannot set-commit on private issue')
1741
1742 if new_state == _CQState.COMMIT:
1743 self.SetFlag('commit', '1')
1744 elif new_state == _CQState.NONE:
1745 self.SetFlag('commit', '0')
1746 else:
1747 raise NotImplementedError()
1748
1749
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001750 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1751 directory):
1752 # TODO(maruel): Use apply_issue.py
1753
1754 # PatchIssue should never be called with a dirty tree. It is up to the
1755 # caller to check this, but just in case we assert here since the
1756 # consequences of the caller not checking this could be dire.
1757 assert(not git_common.is_dirty_git_tree('apply'))
1758 assert(parsed_issue_arg.valid)
1759 self._changelist.issue = parsed_issue_arg.issue
1760 if parsed_issue_arg.hostname:
1761 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1762
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001763 if (isinstance(parsed_issue_arg, _RietveldParsedIssueNumberArgument) and
1764 parsed_issue_arg.patch_url):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001765 assert parsed_issue_arg.patchset
1766 patchset = parsed_issue_arg.patchset
1767 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1768 else:
1769 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1770 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1771
1772 # Switch up to the top-level directory, if necessary, in preparation for
1773 # applying the patch.
1774 top = settings.GetRelativeRoot()
1775 if top:
1776 os.chdir(top)
1777
1778 # Git patches have a/ at the beginning of source paths. We strip that out
1779 # with a sed script rather than the -p flag to patch so we can feed either
1780 # Git or svn-style patches into the same apply command.
1781 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1782 try:
1783 patch_data = subprocess2.check_output(
1784 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1785 except subprocess2.CalledProcessError:
1786 DieWithError('Git patch mungling failed.')
1787 logging.info(patch_data)
1788
1789 # We use "git apply" to apply the patch instead of "patch" so that we can
1790 # pick up file adds.
1791 # The --index flag means: also insert into the index (so we catch adds).
1792 cmd = ['git', 'apply', '--index', '-p0']
1793 if directory:
1794 cmd.extend(('--directory', directory))
1795 if reject:
1796 cmd.append('--reject')
1797 elif IsGitVersionAtLeast('1.7.12'):
1798 cmd.append('--3way')
1799 try:
1800 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
1801 stdin=patch_data, stdout=subprocess2.VOID)
1802 except subprocess2.CalledProcessError:
1803 print 'Failed to apply the patch'
1804 return 1
1805
1806 # If we had an issue, commit the current state and register the issue.
1807 if not nocommit:
1808 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
1809 'patch from issue %(i)s at patchset '
1810 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
1811 % {'i': self.GetIssue(), 'p': patchset})])
1812 self.SetIssue(self.GetIssue())
1813 self.SetPatchset(patchset)
1814 print "Committed patch locally."
1815 else:
1816 print "Patch applied to index."
1817 return 0
1818
1819 @staticmethod
1820 def ParseIssueURL(parsed_url):
1821 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1822 return None
1823 # Typical url: https://domain/<issue_number>[/[other]]
1824 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
1825 if match:
1826 return _RietveldParsedIssueNumberArgument(
1827 issue=int(match.group(1)),
1828 hostname=parsed_url.netloc)
1829 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
1830 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
1831 if match:
1832 return _RietveldParsedIssueNumberArgument(
1833 issue=int(match.group(1)),
1834 patchset=int(match.group(2)),
1835 hostname=parsed_url.netloc,
1836 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
1837 return None
1838
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001839 def CMDUploadChange(self, options, args, change):
1840 """Upload the patch to Rietveld."""
1841 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1842 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001843 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
1844 if options.emulate_svn_auto_props:
1845 upload_args.append('--emulate_svn_auto_props')
1846
1847 change_desc = None
1848
1849 if options.email is not None:
1850 upload_args.extend(['--email', options.email])
1851
1852 if self.GetIssue():
1853 if options.title:
1854 upload_args.extend(['--title', options.title])
1855 if options.message:
1856 upload_args.extend(['--message', options.message])
1857 upload_args.extend(['--issue', str(self.GetIssue())])
1858 print ('This branch is associated with issue %s. '
1859 'Adding patch to that issue.' % self.GetIssue())
1860 else:
1861 if options.title:
1862 upload_args.extend(['--title', options.title])
1863 message = (options.title or options.message or
1864 CreateDescriptionFromLog(args))
1865 change_desc = ChangeDescription(message)
1866 if options.reviewers or options.tbr_owners:
1867 change_desc.update_reviewers(options.reviewers,
1868 options.tbr_owners,
1869 change)
1870 if not options.force:
1871 change_desc.prompt()
1872
1873 if not change_desc.description:
1874 print "Description is empty; aborting."
1875 return 1
1876
1877 upload_args.extend(['--message', change_desc.description])
1878 if change_desc.get_reviewers():
1879 upload_args.append('--reviewers=%s' % ','.join(
1880 change_desc.get_reviewers()))
1881 if options.send_mail:
1882 if not change_desc.get_reviewers():
1883 DieWithError("Must specify reviewers to send email.")
1884 upload_args.append('--send_mail')
1885
1886 # We check this before applying rietveld.private assuming that in
1887 # rietveld.cc only addresses which we can send private CLs to are listed
1888 # if rietveld.private is set, and so we should ignore rietveld.cc only
1889 # when --private is specified explicitly on the command line.
1890 if options.private:
1891 logging.warn('rietveld.cc is ignored since private flag is specified. '
1892 'You need to review and add them manually if necessary.')
1893 cc = self.GetCCListWithoutDefault()
1894 else:
1895 cc = self.GetCCList()
1896 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
1897 if cc:
1898 upload_args.extend(['--cc', cc])
1899
1900 if options.private or settings.GetDefaultPrivateFlag() == "True":
1901 upload_args.append('--private')
1902
1903 upload_args.extend(['--git_similarity', str(options.similarity)])
1904 if not options.find_copies:
1905 upload_args.extend(['--git_no_find_copies'])
1906
1907 # Include the upstream repo's URL in the change -- this is useful for
1908 # projects that have their source spread across multiple repos.
1909 remote_url = self.GetGitBaseUrlFromConfig()
1910 if not remote_url:
1911 if settings.GetIsGitSvn():
1912 remote_url = self.GetGitSvnRemoteUrl()
1913 else:
1914 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
1915 remote_url = '%s@%s' % (self.GetRemoteUrl(),
1916 self.GetUpstreamBranch().split('/')[-1])
1917 if remote_url:
1918 upload_args.extend(['--base_url', remote_url])
1919 remote, remote_branch = self.GetRemoteBranch()
1920 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
1921 settings.GetPendingRefPrefix())
1922 if target_ref:
1923 upload_args.extend(['--target_ref', target_ref])
1924
1925 # Look for dependent patchsets. See crbug.com/480453 for more details.
1926 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
1927 upstream_branch = ShortBranchName(upstream_branch)
1928 if remote is '.':
1929 # A local branch is being tracked.
1930 local_branch = ShortBranchName(upstream_branch)
1931 if settings.GetIsSkipDependencyUpload(local_branch):
1932 print
1933 print ('Skipping dependency patchset upload because git config '
1934 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
1935 print
1936 else:
1937 auth_config = auth.extract_auth_config_from_options(options)
1938 branch_cl = Changelist(branchref=local_branch,
1939 auth_config=auth_config)
1940 branch_cl_issue_url = branch_cl.GetIssueURL()
1941 branch_cl_issue = branch_cl.GetIssue()
1942 branch_cl_patchset = branch_cl.GetPatchset()
1943 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
1944 upload_args.extend(
1945 ['--depends_on_patchset', '%s:%s' % (
1946 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001947 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001948 '\n'
1949 'The current branch (%s) is tracking a local branch (%s) with '
1950 'an associated CL.\n'
1951 'Adding %s/#ps%s as a dependency patchset.\n'
1952 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
1953 branch_cl_patchset))
1954
1955 project = settings.GetProject()
1956 if project:
1957 upload_args.extend(['--project', project])
1958
1959 if options.cq_dry_run:
1960 upload_args.extend(['--cq_dry_run'])
1961
1962 try:
1963 upload_args = ['upload'] + upload_args + args
1964 logging.info('upload.RealMain(%s)', upload_args)
1965 issue, patchset = upload.RealMain(upload_args)
1966 issue = int(issue)
1967 patchset = int(patchset)
1968 except KeyboardInterrupt:
1969 sys.exit(1)
1970 except:
1971 # If we got an exception after the user typed a description for their
1972 # change, back up the description before re-raising.
1973 if change_desc:
1974 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1975 print('\nGot exception while uploading -- saving description to %s\n' %
1976 backup_path)
1977 backup_file = open(backup_path, 'w')
1978 backup_file.write(change_desc.description)
1979 backup_file.close()
1980 raise
1981
1982 if not self.GetIssue():
1983 self.SetIssue(issue)
1984 self.SetPatchset(patchset)
1985
1986 if options.use_commit_queue:
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001987 self.SetCQState(_CQState.COMMIT)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001988 return 0
1989
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001990
1991class _GerritChangelistImpl(_ChangelistCodereviewBase):
1992 def __init__(self, changelist, auth_config=None):
1993 # auth_config is Rietveld thing, kept here to preserve interface only.
1994 super(_GerritChangelistImpl, self).__init__(changelist)
1995 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001996 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001997 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001998 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001999
2000 def _GetGerritHost(self):
2001 # Lazy load of configs.
2002 self.GetCodereviewServer()
2003 return self._gerrit_host
2004
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002005 def _GetGitHost(self):
2006 """Returns git host to be used when uploading change to Gerrit."""
2007 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2008
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002009 def GetCodereviewServer(self):
2010 if not self._gerrit_server:
2011 # If we're on a branch then get the server potentially associated
2012 # with that branch.
2013 if self.GetIssue():
2014 gerrit_server_setting = self.GetCodereviewServerSetting()
2015 if gerrit_server_setting:
2016 self._gerrit_server = RunGit(['config', gerrit_server_setting],
2017 error_ok=True).strip()
2018 if self._gerrit_server:
2019 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
2020 if not self._gerrit_server:
2021 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2022 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002023 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002024 parts[0] = parts[0] + '-review'
2025 self._gerrit_host = '.'.join(parts)
2026 self._gerrit_server = 'https://%s' % self._gerrit_host
2027 return self._gerrit_server
2028
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002029 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00002030 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002031 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002032
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002033 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002034 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002035 if settings.GetGerritSkipEnsureAuthenticated():
2036 # For projects with unusual authentication schemes.
2037 # See http://crbug.com/603378.
2038 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002039 # Lazy-loader to identify Gerrit and Git hosts.
2040 if gerrit_util.GceAuthenticator.is_gce():
2041 return
2042 self.GetCodereviewServer()
2043 git_host = self._GetGitHost()
2044 assert self._gerrit_server and self._gerrit_host
2045 cookie_auth = gerrit_util.CookiesAuthenticator()
2046
2047 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2048 git_auth = cookie_auth.get_auth_header(git_host)
2049 if gerrit_auth and git_auth:
2050 if gerrit_auth == git_auth:
2051 return
2052 print((
2053 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2054 ' Check your %s or %s file for credentials of hosts:\n'
2055 ' %s\n'
2056 ' %s\n'
2057 ' %s') %
2058 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2059 git_host, self._gerrit_host,
2060 cookie_auth.get_new_password_message(git_host)))
2061 if not force:
2062 ask_for_data('If you know what you are doing, press Enter to continue, '
2063 'Ctrl+C to abort.')
2064 return
2065 else:
2066 missing = (
2067 [] if gerrit_auth else [self._gerrit_host] +
2068 [] if git_auth else [git_host])
2069 DieWithError('Credentials for the following hosts are required:\n'
2070 ' %s\n'
2071 'These are read from %s (or legacy %s)\n'
2072 '%s' % (
2073 '\n '.join(missing),
2074 cookie_auth.get_gitcookies_path(),
2075 cookie_auth.get_netrc_path(),
2076 cookie_auth.get_new_password_message(git_host)))
2077
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002078
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002079 def PatchsetSetting(self):
2080 """Return the git setting that stores this change's most recent patchset."""
2081 return 'branch.%s.gerritpatchset' % self.GetBranch()
2082
2083 def GetCodereviewServerSetting(self):
2084 """Returns the git setting that stores this change's Gerrit server."""
2085 branch = self.GetBranch()
2086 if branch:
2087 return 'branch.%s.gerritserver' % branch
2088 return None
2089
2090 def GetRieveldObjForPresubmit(self):
2091 class ThisIsNotRietveldIssue(object):
2092 def __nonzero__(self):
2093 # This is a hack to make presubmit_support think that rietveld is not
2094 # defined, yet still ensure that calls directly result in a decent
2095 # exception message below.
2096 return False
2097
2098 def __getattr__(self, attr):
2099 print(
2100 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2101 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2102 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2103 'or use Rietveld for codereview.\n'
2104 'See also http://crbug.com/579160.' % attr)
2105 raise NotImplementedError()
2106 return ThisIsNotRietveldIssue()
2107
2108 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002109 """Apply a rough heuristic to give a simple summary of an issue's review
2110 or CQ status, assuming adherence to a common workflow.
2111
2112 Returns None if no issue for this branch, or one of the following keywords:
2113 * 'error' - error from review tool (including deleted issues)
2114 * 'unsent' - no reviewers added
2115 * 'waiting' - waiting for review
2116 * 'reply' - waiting for owner to reply to review
2117 * 'not lgtm' - Code-Review -2 from at least one approved reviewer
2118 * 'lgtm' - Code-Review +2 from at least one approved reviewer
2119 * 'commit' - in the commit queue
2120 * 'closed' - abandoned
2121 """
2122 if not self.GetIssue():
2123 return None
2124
2125 try:
2126 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
2127 except httplib.HTTPException:
2128 return 'error'
2129
2130 if data['status'] == 'ABANDONED':
2131 return 'closed'
2132
2133 cq_label = data['labels'].get('Commit-Queue', {})
2134 if cq_label:
2135 # Vote value is a stringified integer, which we expect from 0 to 2.
2136 vote_value = cq_label.get('value', '0')
2137 vote_text = cq_label.get('values', {}).get(vote_value, '')
2138 if vote_text.lower() == 'commit':
2139 return 'commit'
2140
2141 lgtm_label = data['labels'].get('Code-Review', {})
2142 if lgtm_label:
2143 if 'rejected' in lgtm_label:
2144 return 'not lgtm'
2145 if 'approved' in lgtm_label:
2146 return 'lgtm'
2147
2148 if not data.get('reviewers', {}).get('REVIEWER', []):
2149 return 'unsent'
2150
2151 messages = data.get('messages', [])
2152 if messages:
2153 owner = data['owner'].get('_account_id')
2154 last_message_author = messages[-1].get('author', {}).get('_account_id')
2155 if owner != last_message_author:
2156 # Some reply from non-owner.
2157 return 'reply'
2158
2159 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002160
2161 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002162 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002163 return data['revisions'][data['current_revision']]['_number']
2164
2165 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002166 data = self._GetChangeDetail(['CURRENT_REVISION'])
2167 current_rev = data['current_revision']
2168 url = data['revisions'][current_rev]['fetch']['http']['url']
2169 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002170
2171 def UpdateDescriptionRemote(self, description):
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002172 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
2173 description)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002174
2175 def CloseIssue(self):
2176 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2177
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002178 def GetApprovingReviewers(self):
2179 """Returns a list of reviewers approving the change.
2180
2181 Note: not necessarily committers.
2182 """
2183 raise NotImplementedError()
2184
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002185 def SubmitIssue(self, wait_for_merge=True):
2186 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2187 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002188
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002189 def _GetChangeDetail(self, options=None, issue=None):
2190 options = options or []
2191 issue = issue or self.GetIssue()
2192 assert issue, 'issue required to query Gerrit'
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002193 return gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2194 options)
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002195
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002196 def CMDLand(self, force, bypass_hooks, verbose):
2197 if git_common.is_dirty_git_tree('land'):
2198 return 1
2199 differs = True
2200 last_upload = RunGit(['config',
2201 'branch.%s.gerritsquashhash' % self.GetBranch()],
2202 error_ok=True).strip()
2203 # Note: git diff outputs nothing if there is no diff.
2204 if not last_upload or RunGit(['diff', last_upload]).strip():
2205 print('WARNING: some changes from local branch haven\'t been uploaded')
2206 else:
2207 detail = self._GetChangeDetail(['CURRENT_REVISION'])
2208 if detail['current_revision'] == last_upload:
2209 differs = False
2210 else:
2211 print('WARNING: local branch contents differ from latest uploaded '
2212 'patchset')
2213 if differs:
2214 if not force:
2215 ask_for_data(
2216 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2217 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2218 elif not bypass_hooks:
2219 hook_results = self.RunHook(
2220 committing=True,
2221 may_prompt=not force,
2222 verbose=verbose,
2223 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2224 if not hook_results.should_continue():
2225 return 1
2226
2227 self.SubmitIssue(wait_for_merge=True)
2228 print('Issue %s has been submitted.' % self.GetIssueURL())
2229 return 0
2230
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002231 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2232 directory):
2233 assert not reject
2234 assert not nocommit
2235 assert not directory
2236 assert parsed_issue_arg.valid
2237
2238 self._changelist.issue = parsed_issue_arg.issue
2239
2240 if parsed_issue_arg.hostname:
2241 self._gerrit_host = parsed_issue_arg.hostname
2242 self._gerrit_server = 'https://%s' % self._gerrit_host
2243
2244 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2245
2246 if not parsed_issue_arg.patchset:
2247 # Use current revision by default.
2248 revision_info = detail['revisions'][detail['current_revision']]
2249 patchset = int(revision_info['_number'])
2250 else:
2251 patchset = parsed_issue_arg.patchset
2252 for revision_info in detail['revisions'].itervalues():
2253 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2254 break
2255 else:
2256 DieWithError('Couldn\'t find patchset %i in issue %i' %
2257 (parsed_issue_arg.patchset, self.GetIssue()))
2258
2259 fetch_info = revision_info['fetch']['http']
2260 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2261 RunGit(['cherry-pick', 'FETCH_HEAD'])
2262 self.SetIssue(self.GetIssue())
2263 self.SetPatchset(patchset)
2264 print('Committed patch for issue %i pathset %i locally' %
2265 (self.GetIssue(), self.GetPatchset()))
2266 return 0
2267
2268 @staticmethod
2269 def ParseIssueURL(parsed_url):
2270 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2271 return None
2272 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2273 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2274 # Short urls like https://domain/<issue_number> can be used, but don't allow
2275 # specifying the patchset (you'd 404), but we allow that here.
2276 if parsed_url.path == '/':
2277 part = parsed_url.fragment
2278 else:
2279 part = parsed_url.path
2280 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2281 if match:
2282 return _ParsedIssueNumberArgument(
2283 issue=int(match.group(2)),
2284 patchset=int(match.group(4)) if match.group(4) else None,
2285 hostname=parsed_url.netloc)
2286 return None
2287
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002288 def CMDUploadChange(self, options, args, change):
2289 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002290 if options.squash and options.no_squash:
2291 DieWithError('Can only use one of --squash or --no-squash')
2292 options.squash = ((settings.GetSquashGerritUploads() or options.squash) and
2293 not options.no_squash)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002294 # We assume the remote called "origin" is the one we want.
2295 # It is probably not worthwhile to support different workflows.
2296 gerrit_remote = 'origin'
2297
2298 remote, remote_branch = self.GetRemoteBranch()
2299 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2300 pending_prefix='')
2301
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002302 if options.squash:
2303 if not self.GetIssue():
2304 # TODO(tandrii): deperecate this after 2016Q2. Backwards compatibility
2305 # with shadow branch, which used to contain change-id for a given
2306 # branch, using which we can fetch actual issue number and set it as the
2307 # property of the branch, which is the new way.
2308 message = RunGitSilent([
2309 'show', '--format=%B', '-s',
2310 'refs/heads/git_cl_uploads/%s' % self.GetBranch()])
2311 if message:
2312 change_ids = git_footers.get_footer_change_id(message.strip())
2313 if change_ids and len(change_ids) == 1:
2314 details = self._GetChangeDetail(issue=change_ids[0])
2315 if details:
2316 print('WARNING: found old upload in branch git_cl_uploads/%s '
2317 'corresponding to issue %s' %
2318 (self.GetBranch(), details['_number']))
2319 self.SetIssue(details['_number'])
2320 if not self.GetIssue():
2321 DieWithError(
2322 '\n' # For readability of the blob below.
2323 'Found old upload in branch git_cl_uploads/%s, '
2324 'but failed to find corresponding Gerrit issue.\n'
2325 'If you know the issue number, set it manually first:\n'
2326 ' git cl issue 123456\n'
2327 'If you intended to upload this CL as new issue, '
2328 'just delete or rename the old upload branch:\n'
2329 ' git rename-branch git_cl_uploads/%s old_upload-%s\n'
2330 'After that, please run git cl upload again.' %
2331 tuple([self.GetBranch()] * 3))
2332 # End of backwards compatability.
2333
2334 if self.GetIssue():
2335 # Try to get the message from a previous upload.
2336 message = self.GetDescription()
2337 if not message:
2338 DieWithError(
2339 'failed to fetch description from current Gerrit issue %d\n'
2340 '%s' % (self.GetIssue(), self.GetIssueURL()))
2341 change_id = self._GetChangeDetail()['change_id']
2342 while True:
2343 footer_change_ids = git_footers.get_footer_change_id(message)
2344 if footer_change_ids == [change_id]:
2345 break
2346 if not footer_change_ids:
2347 message = git_footers.add_footer_change_id(message, change_id)
2348 print('WARNING: appended missing Change-Id to issue description')
2349 continue
2350 # There is already a valid footer but with different or several ids.
2351 # Doing this automatically is non-trivial as we don't want to lose
2352 # existing other footers, yet we want to append just 1 desired
2353 # Change-Id. Thus, just create a new footer, but let user verify the
2354 # new description.
2355 message = '%s\n\nChange-Id: %s' % (message, change_id)
2356 print(
2357 'WARNING: issue %s has Change-Id footer(s):\n'
2358 ' %s\n'
2359 'but issue has Change-Id %s, according to Gerrit.\n'
2360 'Please, check the proposed correction to the description, '
2361 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2362 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2363 change_id))
2364 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2365 if not options.force:
2366 change_desc = ChangeDescription(message)
2367 change_desc.prompt()
2368 message = change_desc.description
2369 if not message:
2370 DieWithError("Description is empty. Aborting...")
2371 # Continue the while loop.
2372 # Sanity check of this code - we should end up with proper message
2373 # footer.
2374 assert [change_id] == git_footers.get_footer_change_id(message)
2375 change_desc = ChangeDescription(message)
2376 else:
2377 change_desc = ChangeDescription(
2378 options.message or CreateDescriptionFromLog(args))
2379 if not options.force:
2380 change_desc.prompt()
2381 if not change_desc.description:
2382 DieWithError("Description is empty. Aborting...")
2383 message = change_desc.description
2384 change_ids = git_footers.get_footer_change_id(message)
2385 if len(change_ids) > 1:
2386 DieWithError('too many Change-Id footers, at most 1 allowed.')
2387 if not change_ids:
2388 # Generate the Change-Id automatically.
2389 message = git_footers.add_footer_change_id(
2390 message, GenerateGerritChangeId(message))
2391 change_desc.set_description(message)
2392 change_ids = git_footers.get_footer_change_id(message)
2393 assert len(change_ids) == 1
2394 change_id = change_ids[0]
2395
2396 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2397 if remote is '.':
2398 # If our upstream branch is local, we base our squashed commit on its
2399 # squashed version.
2400 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2401 # Check the squashed hash of the parent.
2402 parent = RunGit(['config',
2403 'branch.%s.gerritsquashhash' % upstream_branch_name],
2404 error_ok=True).strip()
2405 # Verify that the upstream branch has been uploaded too, otherwise
2406 # Gerrit will create additional CLs when uploading.
2407 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2408 RunGitSilent(['rev-parse', parent + ':'])):
2409 # TODO(tandrii): remove "old depot_tools" part on April 12, 2016.
2410 DieWithError(
2411 'Upload upstream branch %s first.\n'
2412 'Note: maybe you\'ve uploaded it with --no-squash or with an old '
2413 'version of depot_tools. If so, then re-upload it with:\n'
2414 ' git cl upload --squash\n' % upstream_branch_name)
2415 else:
2416 parent = self.GetCommonAncestorWithUpstream()
2417
2418 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2419 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2420 '-m', message]).strip()
2421 else:
2422 change_desc = ChangeDescription(
2423 options.message or CreateDescriptionFromLog(args))
2424 if not change_desc.description:
2425 DieWithError("Description is empty. Aborting...")
2426
2427 if not git_footers.get_footer_change_id(change_desc.description):
2428 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002429 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2430 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002431 ref_to_push = 'HEAD'
2432 parent = '%s/%s' % (gerrit_remote, branch)
2433 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2434
2435 assert change_desc
2436 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2437 ref_to_push)]).splitlines()
2438 if len(commits) > 1:
2439 print('WARNING: This will upload %d commits. Run the following command '
2440 'to see which commits will be uploaded: ' % len(commits))
2441 print('git log %s..%s' % (parent, ref_to_push))
2442 print('You can also use `git squash-branch` to squash these into a '
2443 'single commit.')
2444 ask_for_data('About to upload; enter to confirm.')
2445
2446 if options.reviewers or options.tbr_owners:
2447 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2448 change)
2449
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002450 # Extra options that can be specified at push time. Doc:
2451 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2452 refspec_opts = []
2453 if options.title:
2454 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2455 # reverse on its side.
2456 if '_' in options.title:
2457 print('WARNING: underscores in title will be converted to spaces.')
2458 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2459
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002460 cc = self.GetCCList().split(',')
2461 if options.cc:
2462 cc.extend(options.cc)
2463 cc = filter(None, cc)
2464 if cc:
tandrii@chromium.org0b2d7072016-04-18 16:19:03 +00002465 # refspec_opts.extend('cc=' + email.strip() for email in cc)
2466 # TODO(tandrii): enable this back. http://crbug.com/604377
2467 print('WARNING: Gerrit doesn\'t yet support cc-ing arbitrary emails.\n'
2468 ' Ignoring cc-ed emails. See http://crbug.com/604377.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002469
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002470 if change_desc.get_reviewers():
2471 refspec_opts.extend('r=' + email.strip()
2472 for email in change_desc.get_reviewers())
2473
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002474
2475 refspec_suffix = ''
2476 if refspec_opts:
2477 refspec_suffix = '%' + ','.join(refspec_opts)
2478 assert ' ' not in refspec_suffix, (
2479 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002480 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002481
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002482 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002483 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002484 print_stdout=True,
2485 # Flush after every line: useful for seeing progress when running as
2486 # recipe.
2487 filter_fn=lambda _: sys.stdout.flush())
2488
2489 if options.squash:
2490 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2491 change_numbers = [m.group(1)
2492 for m in map(regex.match, push_stdout.splitlines())
2493 if m]
2494 if len(change_numbers) != 1:
2495 DieWithError(
2496 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2497 'Change-Id: %s') % (len(change_numbers), change_id))
2498 self.SetIssue(change_numbers[0])
2499 RunGit(['config', 'branch.%s.gerritsquashhash' % self.GetBranch(),
2500 ref_to_push])
2501 return 0
2502
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002503 def _AddChangeIdToCommitMessage(self, options, args):
2504 """Re-commits using the current message, assumes the commit hook is in
2505 place.
2506 """
2507 log_desc = options.message or CreateDescriptionFromLog(args)
2508 git_command = ['commit', '--amend', '-m', log_desc]
2509 RunGit(git_command)
2510 new_log_desc = CreateDescriptionFromLog(args)
2511 if git_footers.get_footer_change_id(new_log_desc):
2512 print 'git-cl: Added Change-Id to commit message.'
2513 return new_log_desc
2514 else:
2515 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002516
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002517 def SetCQState(self, new_state):
2518 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
2519 # TODO(tandrii): maybe allow configurability in codereview.settings or by
2520 # self-discovery of label config for this CL using REST API.
2521 vote_map = {
2522 _CQState.NONE: 0,
2523 _CQState.DRY_RUN: 1,
2524 _CQState.COMMIT : 2,
2525 }
2526 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2527 labels={'Commit-Queue': vote_map[new_state]})
2528
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002529
2530_CODEREVIEW_IMPLEMENTATIONS = {
2531 'rietveld': _RietveldChangelistImpl,
2532 'gerrit': _GerritChangelistImpl,
2533}
2534
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002535
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002536def _add_codereview_select_options(parser):
2537 """Appends --gerrit and --rietveld options to force specific codereview."""
2538 parser.codereview_group = optparse.OptionGroup(
2539 parser, 'EXPERIMENTAL! Codereview override options')
2540 parser.add_option_group(parser.codereview_group)
2541 parser.codereview_group.add_option(
2542 '--gerrit', action='store_true',
2543 help='Force the use of Gerrit for codereview')
2544 parser.codereview_group.add_option(
2545 '--rietveld', action='store_true',
2546 help='Force the use of Rietveld for codereview')
2547
2548
2549def _process_codereview_select_options(parser, options):
2550 if options.gerrit and options.rietveld:
2551 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2552 options.forced_codereview = None
2553 if options.gerrit:
2554 options.forced_codereview = 'gerrit'
2555 elif options.rietveld:
2556 options.forced_codereview = 'rietveld'
2557
2558
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002559class ChangeDescription(object):
2560 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002561 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002562 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002563
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002564 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002565 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002566
agable@chromium.org42c20792013-09-12 17:34:49 +00002567 @property # www.logilab.org/ticket/89786
2568 def description(self): # pylint: disable=E0202
2569 return '\n'.join(self._description_lines)
2570
2571 def set_description(self, desc):
2572 if isinstance(desc, basestring):
2573 lines = desc.splitlines()
2574 else:
2575 lines = [line.rstrip() for line in desc]
2576 while lines and not lines[0]:
2577 lines.pop(0)
2578 while lines and not lines[-1]:
2579 lines.pop(-1)
2580 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002581
piman@chromium.org336f9122014-09-04 02:16:55 +00002582 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002583 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002584 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002585 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002586 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002587 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002588
agable@chromium.org42c20792013-09-12 17:34:49 +00002589 # Get the set of R= and TBR= lines and remove them from the desciption.
2590 regexp = re.compile(self.R_LINE)
2591 matches = [regexp.match(line) for line in self._description_lines]
2592 new_desc = [l for i, l in enumerate(self._description_lines)
2593 if not matches[i]]
2594 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002595
agable@chromium.org42c20792013-09-12 17:34:49 +00002596 # Construct new unified R= and TBR= lines.
2597 r_names = []
2598 tbr_names = []
2599 for match in matches:
2600 if not match:
2601 continue
2602 people = cleanup_list([match.group(2).strip()])
2603 if match.group(1) == 'TBR':
2604 tbr_names.extend(people)
2605 else:
2606 r_names.extend(people)
2607 for name in r_names:
2608 if name not in reviewers:
2609 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002610 if add_owners_tbr:
2611 owners_db = owners.Database(change.RepositoryRoot(),
2612 fopen=file, os_path=os.path, glob=glob.glob)
2613 all_reviewers = set(tbr_names + reviewers)
2614 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2615 all_reviewers)
2616 tbr_names.extend(owners_db.reviewers_for(missing_files,
2617 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002618 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2619 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2620
2621 # Put the new lines in the description where the old first R= line was.
2622 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2623 if 0 <= line_loc < len(self._description_lines):
2624 if new_tbr_line:
2625 self._description_lines.insert(line_loc, new_tbr_line)
2626 if new_r_line:
2627 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002628 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002629 if new_r_line:
2630 self.append_footer(new_r_line)
2631 if new_tbr_line:
2632 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002633
2634 def prompt(self):
2635 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002636 self.set_description([
2637 '# Enter a description of the change.',
2638 '# This will be displayed on the codereview site.',
2639 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002640 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002641 '--------------------',
2642 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002643
agable@chromium.org42c20792013-09-12 17:34:49 +00002644 regexp = re.compile(self.BUG_LINE)
2645 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00002646 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00002647 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002648 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002649 if not content:
2650 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002651 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002652
2653 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002654 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2655 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002656 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002657 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002658
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002659 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00002660 if self._description_lines:
2661 # Add an empty line if either the last line or the new line isn't a tag.
2662 last_line = self._description_lines[-1]
2663 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
2664 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2665 self._description_lines.append('')
2666 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002667
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002668 def get_reviewers(self):
2669 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002670 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
2671 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002672 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002673
2674
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002675def get_approving_reviewers(props):
2676 """Retrieves the reviewers that approved a CL from the issue properties with
2677 messages.
2678
2679 Note that the list may contain reviewers that are not committer, thus are not
2680 considered by the CQ.
2681 """
2682 return sorted(
2683 set(
2684 message['sender']
2685 for message in props['messages']
2686 if message['approval'] and message['sender'] in props['reviewers']
2687 )
2688 )
2689
2690
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002691def FindCodereviewSettingsFile(filename='codereview.settings'):
2692 """Finds the given file starting in the cwd and going up.
2693
2694 Only looks up to the top of the repository unless an
2695 'inherit-review-settings-ok' file exists in the root of the repository.
2696 """
2697 inherit_ok_file = 'inherit-review-settings-ok'
2698 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002699 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002700 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2701 root = '/'
2702 while True:
2703 if filename in os.listdir(cwd):
2704 if os.path.isfile(os.path.join(cwd, filename)):
2705 return open(os.path.join(cwd, filename))
2706 if cwd == root:
2707 break
2708 cwd = os.path.dirname(cwd)
2709
2710
2711def LoadCodereviewSettingsFromFile(fileobj):
2712 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002713 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002714
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002715 def SetProperty(name, setting, unset_error_ok=False):
2716 fullname = 'rietveld.' + name
2717 if setting in keyvals:
2718 RunGit(['config', fullname, keyvals[setting]])
2719 else:
2720 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2721
2722 SetProperty('server', 'CODE_REVIEW_SERVER')
2723 # Only server setting is required. Other settings can be absent.
2724 # In that case, we ignore errors raised during option deletion attempt.
2725 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002726 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002727 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2728 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002729 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002730 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002731 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2732 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002733 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002734 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002735 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002736 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2737 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002738
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002739 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002740 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002741
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002742 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
2743 RunGit(['config', 'gerrit.squash-uploads',
2744 keyvals['GERRIT_SQUASH_UPLOADS']])
2745
tandrii@chromium.org28253532016-04-14 13:46:56 +00002746 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00002747 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00002748 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
2749
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002750 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2751 #should be of the form
2752 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2753 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2754 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2755 keyvals['ORIGIN_URL_CONFIG']])
2756
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002757
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002758def urlretrieve(source, destination):
2759 """urllib is broken for SSL connections via a proxy therefore we
2760 can't use urllib.urlretrieve()."""
2761 with open(destination, 'w') as f:
2762 f.write(urllib2.urlopen(source).read())
2763
2764
ukai@chromium.org712d6102013-11-27 00:52:58 +00002765def hasSheBang(fname):
2766 """Checks fname is a #! script."""
2767 with open(fname) as f:
2768 return f.read(2).startswith('#!')
2769
2770
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00002771# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
2772def DownloadHooks(*args, **kwargs):
2773 pass
2774
2775
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002776def DownloadGerritHook(force):
2777 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002778
2779 Args:
2780 force: True to update hooks. False to install hooks if not present.
2781 """
2782 if not settings.GetIsGerrit():
2783 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00002784 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002785 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2786 if not os.access(dst, os.X_OK):
2787 if os.path.exists(dst):
2788 if not force:
2789 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002790 try:
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002791 print(
2792 'WARNING: installing Gerrit commit-msg hook.\n'
2793 ' This behavior of git cl will soon be disabled.\n'
2794 ' See bug http://crbug.com/579176.')
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002795 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002796 if not hasSheBang(dst):
2797 DieWithError('Not a script: %s\n'
2798 'You need to download from\n%s\n'
2799 'into .git/hooks/commit-msg and '
2800 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002801 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2802 except Exception:
2803 if os.path.exists(dst):
2804 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002805 DieWithError('\nFailed to download hooks.\n'
2806 'You need to download from\n%s\n'
2807 'into .git/hooks/commit-msg and '
2808 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002809
2810
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002811
2812def GetRietveldCodereviewSettingsInteractively():
2813 """Prompt the user for settings."""
2814 server = settings.GetDefaultServerUrl(error_ok=True)
2815 prompt = 'Rietveld server (host[:port])'
2816 prompt += ' [%s]' % (server or DEFAULT_SERVER)
2817 newserver = ask_for_data(prompt + ':')
2818 if not server and not newserver:
2819 newserver = DEFAULT_SERVER
2820 if newserver:
2821 newserver = gclient_utils.UpgradeToHttps(newserver)
2822 if newserver != server:
2823 RunGit(['config', 'rietveld.server', newserver])
2824
2825 def SetProperty(initial, caption, name, is_url):
2826 prompt = caption
2827 if initial:
2828 prompt += ' ("x" to clear) [%s]' % initial
2829 new_val = ask_for_data(prompt + ':')
2830 if new_val == 'x':
2831 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
2832 elif new_val:
2833 if is_url:
2834 new_val = gclient_utils.UpgradeToHttps(new_val)
2835 if new_val != initial:
2836 RunGit(['config', 'rietveld.' + name, new_val])
2837
2838 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
2839 SetProperty(settings.GetDefaultPrivateFlag(),
2840 'Private flag (rietveld only)', 'private', False)
2841 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
2842 'tree-status-url', False)
2843 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
2844 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
2845 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
2846 'run-post-upload-hook', False)
2847
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002848@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002849def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002850 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002851
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002852 print('WARNING: git cl config works for Rietveld only.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002853 'For Gerrit, see http://crbug.com/603116.')
2854 # TODO(tandrii): add Gerrit support as part of http://crbug.com/603116.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00002855 parser.add_option('--activate-update', action='store_true',
2856 help='activate auto-updating [rietveld] section in '
2857 '.git/config')
2858 parser.add_option('--deactivate-update', action='store_true',
2859 help='deactivate auto-updating [rietveld] section in '
2860 '.git/config')
2861 options, args = parser.parse_args(args)
2862
2863 if options.deactivate_update:
2864 RunGit(['config', 'rietveld.autoupdate', 'false'])
2865 return
2866
2867 if options.activate_update:
2868 RunGit(['config', '--unset', 'rietveld.autoupdate'])
2869 return
2870
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002871 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002872 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002873 return 0
2874
2875 url = args[0]
2876 if not url.endswith('codereview.settings'):
2877 url = os.path.join(url, 'codereview.settings')
2878
2879 # Load code review settings and download hooks (if available).
2880 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
2881 return 0
2882
2883
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002884def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002885 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002886 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
2887 branch = ShortBranchName(branchref)
2888 _, args = parser.parse_args(args)
2889 if not args:
2890 print("Current base-url:")
2891 return RunGit(['config', 'branch.%s.base-url' % branch],
2892 error_ok=False).strip()
2893 else:
2894 print("Setting base-url to %s" % args[0])
2895 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
2896 error_ok=False).strip()
2897
2898
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002899def color_for_status(status):
2900 """Maps a Changelist status to color, for CMDstatus and other tools."""
2901 return {
2902 'unsent': Fore.RED,
2903 'waiting': Fore.BLUE,
2904 'reply': Fore.YELLOW,
2905 'lgtm': Fore.GREEN,
2906 'commit': Fore.MAGENTA,
2907 'closed': Fore.CYAN,
2908 'error': Fore.WHITE,
2909 }.get(status, Fore.WHITE)
2910
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00002911def fetch_cl_status(branch, auth_config=None):
2912 """Fetches information for an issue and returns (branch, issue, status)."""
2913 cl = Changelist(branchref=branch, auth_config=auth_config)
2914 url = cl.GetIssueURL()
2915 status = cl.GetStatus()
2916
2917 if url and (not status or status == 'error'):
2918 # The issue probably doesn't exist anymore.
2919 url += ' (broken)'
2920
2921 return (branch, url, status)
2922
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002923def get_cl_statuses(
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00002924 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002925 """Returns a blocking iterable of (branch, issue, color) for given branches.
2926
2927 If fine_grained is true, this will fetch CL statuses from the server.
2928 Otherwise, simply indicate if there's a matching url for the given branches.
2929
2930 If max_processes is specified, it is used as the maximum number of processes
2931 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
2932 spawned.
2933 """
2934 # Silence upload.py otherwise it becomes unwieldly.
2935 upload.verbosity = 0
2936
2937 if fine_grained:
2938 # Process one branch synchronously to work through authentication, then
2939 # spawn processes to process all the other branches in parallel.
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00002940 if branches:
2941 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
2942 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002943
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00002944 branches_to_fetch = branches[1:]
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002945 pool = ThreadPool(
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00002946 min(max_processes, len(branches_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002947 if max_processes is not None
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00002948 else len(branches_to_fetch))
2949 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002950 yield x
2951 else:
2952 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00002953 for b in branches:
2954 cl = Changelist(branchref=b, auth_config=auth_config)
2955 url = cl.GetIssueURL()
2956 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002957
rmistry@google.com2dd99862015-06-22 12:22:18 +00002958
2959def upload_branch_deps(cl, args):
2960 """Uploads CLs of local branches that are dependents of the current branch.
2961
2962 If the local branch dependency tree looks like:
2963 test1 -> test2.1 -> test3.1
2964 -> test3.2
2965 -> test2.2 -> test3.3
2966
2967 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
2968 run on the dependent branches in this order:
2969 test2.1, test3.1, test3.2, test2.2, test3.3
2970
2971 Note: This function does not rebase your local dependent branches. Use it when
2972 you make a change to the parent branch that will not conflict with its
2973 dependent branches, and you would like their dependencies updated in
2974 Rietveld.
2975 """
2976 if git_common.is_dirty_git_tree('upload-branch-deps'):
2977 return 1
2978
2979 root_branch = cl.GetBranch()
2980 if root_branch is None:
2981 DieWithError('Can\'t find dependent branches from detached HEAD state. '
2982 'Get on a branch!')
2983 if not cl.GetIssue() or not cl.GetPatchset():
2984 DieWithError('Current branch does not have an uploaded CL. We cannot set '
2985 'patchset dependencies without an uploaded CL.')
2986
2987 branches = RunGit(['for-each-ref',
2988 '--format=%(refname:short) %(upstream:short)',
2989 'refs/heads'])
2990 if not branches:
2991 print('No local branches found.')
2992 return 0
2993
2994 # Create a dictionary of all local branches to the branches that are dependent
2995 # on it.
2996 tracked_to_dependents = collections.defaultdict(list)
2997 for b in branches.splitlines():
2998 tokens = b.split()
2999 if len(tokens) == 2:
3000 branch_name, tracked = tokens
3001 tracked_to_dependents[tracked].append(branch_name)
3002
3003 print
3004 print 'The dependent local branches of %s are:' % root_branch
3005 dependents = []
3006 def traverse_dependents_preorder(branch, padding=''):
3007 dependents_to_process = tracked_to_dependents.get(branch, [])
3008 padding += ' '
3009 for dependent in dependents_to_process:
3010 print '%s%s' % (padding, dependent)
3011 dependents.append(dependent)
3012 traverse_dependents_preorder(dependent, padding)
3013 traverse_dependents_preorder(root_branch)
3014 print
3015
3016 if not dependents:
3017 print 'There are no dependent local branches for %s' % root_branch
3018 return 0
3019
3020 print ('This command will checkout all dependent branches and run '
3021 '"git cl upload".')
3022 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3023
andybons@chromium.org962f9462016-02-03 20:00:42 +00003024 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003025 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003026 args.extend(['-t', 'Updated patchset dependency'])
3027
rmistry@google.com2dd99862015-06-22 12:22:18 +00003028 # Record all dependents that failed to upload.
3029 failures = {}
3030 # Go through all dependents, checkout the branch and upload.
3031 try:
3032 for dependent_branch in dependents:
3033 print
3034 print '--------------------------------------'
3035 print 'Running "git cl upload" from %s:' % dependent_branch
3036 RunGit(['checkout', '-q', dependent_branch])
3037 print
3038 try:
3039 if CMDupload(OptionParser(), args) != 0:
3040 print 'Upload failed for %s!' % dependent_branch
3041 failures[dependent_branch] = 1
3042 except: # pylint: disable=W0702
3043 failures[dependent_branch] = 1
3044 print
3045 finally:
3046 # Swap back to the original root branch.
3047 RunGit(['checkout', '-q', root_branch])
3048
3049 print
3050 print 'Upload complete for dependent branches!'
3051 for dependent_branch in dependents:
3052 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
3053 print ' %s : %s' % (dependent_branch, upload_status)
3054 print
3055
3056 return 0
3057
3058
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003059def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003060 """Show status of changelists.
3061
3062 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003063 - Red not sent for review or broken
3064 - Blue waiting for review
3065 - Yellow waiting for you to reply to review
3066 - Green LGTM'ed
3067 - Magenta in the commit queue
3068 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003069
3070 Also see 'git cl comments'.
3071 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003072 parser.add_option('--field',
3073 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003074 parser.add_option('-f', '--fast', action='store_true',
3075 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003076 parser.add_option(
3077 '-j', '--maxjobs', action='store', type=int,
3078 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003079
3080 auth.add_auth_options(parser)
3081 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003082 if args:
3083 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003084 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003085
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003086 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003087 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003088 if options.field.startswith('desc'):
3089 print cl.GetDescription()
3090 elif options.field == 'id':
3091 issueid = cl.GetIssue()
3092 if issueid:
3093 print issueid
3094 elif options.field == 'patch':
3095 patchset = cl.GetPatchset()
3096 if patchset:
3097 print patchset
3098 elif options.field == 'url':
3099 url = cl.GetIssueURL()
3100 if url:
3101 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003102 return 0
3103
3104 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3105 if not branches:
3106 print('No local branch found.')
3107 return 0
3108
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003109 changes = (
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003110 Changelist(branchref=b, auth_config=auth_config)
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003111 for b in branches.splitlines())
3112 # TODO(tandrii): refactor to use CLs list instead of branches list.
3113 branches = [c.GetBranch() for c in changes]
3114 alignment = max(5, max(len(b) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003115 print 'Branches associated with reviews:'
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003116 output = get_cl_statuses(branches,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003117 fine_grained=not options.fast,
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003118 max_processes=options.maxjobs,
3119 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003120
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003121 branch_statuses = {}
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003122 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
3123 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003124 while branch not in branch_statuses:
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003125 b, i, status = output.next()
3126 branch_statuses[b] = (i, status)
3127 issue_url, status = branch_statuses.pop(branch)
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003128 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003129 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003130 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003131 color = ''
3132 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003133 status_str = '(%s)' % status if status else ''
3134 print ' %*s : %s%s %s%s' % (
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003135 alignment, ShortBranchName(branch), color, issue_url, status_str,
3136 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003137
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003138 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003139 print
3140 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003141 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003142 if not cl.GetIssue():
3143 print 'No issue assigned.'
3144 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003145 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00003146 if not options.fast:
3147 print 'Issue description:'
3148 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003149 return 0
3150
3151
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003152def colorize_CMDstatus_doc():
3153 """To be called once in main() to add colors to git cl status help."""
3154 colors = [i for i in dir(Fore) if i[0].isupper()]
3155
3156 def colorize_line(line):
3157 for color in colors:
3158 if color in line.upper():
3159 # Extract whitespaces first and the leading '-'.
3160 indent = len(line) - len(line.lstrip(' ')) + 1
3161 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3162 return line
3163
3164 lines = CMDstatus.__doc__.splitlines()
3165 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3166
3167
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003168@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003169def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003170 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003171
3172 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003173 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003174 parser.add_option('-r', '--reverse', action='store_true',
3175 help='Lookup the branch(es) for the specified issues. If '
3176 'no issues are specified, all branches with mapped '
3177 'issues will be listed.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003178 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003179 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003180 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003181
dnj@chromium.org406c4402015-03-03 17:22:28 +00003182 if options.reverse:
3183 branches = RunGit(['for-each-ref', 'refs/heads',
3184 '--format=%(refname:short)']).splitlines()
3185
3186 # Reverse issue lookup.
3187 issue_branch_map = {}
3188 for branch in branches:
3189 cl = Changelist(branchref=branch)
3190 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3191 if not args:
3192 args = sorted(issue_branch_map.iterkeys())
3193 for issue in args:
3194 if not issue:
3195 continue
3196 print 'Branch for issue number %s: %s' % (
3197 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
3198 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003199 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003200 if len(args) > 0:
3201 try:
3202 issue = int(args[0])
3203 except ValueError:
3204 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003205 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003206 cl.SetIssue(issue)
3207 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003208 return 0
3209
3210
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003211def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003212 """Shows or posts review comments for any changelist."""
3213 parser.add_option('-a', '--add-comment', dest='comment',
3214 help='comment to add to an issue')
3215 parser.add_option('-i', dest='issue',
3216 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003217 parser.add_option('-j', '--json-file',
3218 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003219 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003220 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003221 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003222
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003223 issue = None
3224 if options.issue:
3225 try:
3226 issue = int(options.issue)
3227 except ValueError:
3228 DieWithError('A review issue id is expected to be a number')
3229
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003230 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003231
3232 if options.comment:
3233 cl.AddComment(options.comment)
3234 return 0
3235
3236 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003237 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003238 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003239 summary.append({
3240 'date': message['date'],
3241 'lgtm': False,
3242 'message': message['text'],
3243 'not_lgtm': False,
3244 'sender': message['sender'],
3245 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003246 if message['disapproval']:
3247 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003248 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003249 elif message['approval']:
3250 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003251 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003252 elif message['sender'] == data['owner_email']:
3253 color = Fore.MAGENTA
3254 else:
3255 color = Fore.BLUE
3256 print '\n%s%s %s%s' % (
3257 color, message['date'].split('.', 1)[0], message['sender'],
3258 Fore.RESET)
3259 if message['text'].strip():
3260 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00003261 if options.json_file:
3262 with open(options.json_file, 'wb') as f:
3263 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003264 return 0
3265
3266
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003267@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003268def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003269 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003270 parser.add_option('-d', '--display', action='store_true',
3271 help='Display the description instead of opening an editor')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003272
3273 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003274 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003275 options, args = parser.parse_args(args)
3276 _process_codereview_select_options(parser, options)
3277
3278 target_issue = None
3279 if len(args) > 0:
3280 issue_arg = ParseIssueNumberArgument(args[0])
3281 if not issue_arg.valid:
3282 parser.print_help()
3283 return 1
3284 target_issue = issue_arg.issue
3285
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003286 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003287
3288 cl = Changelist(
3289 auth_config=auth_config, issue=target_issue,
3290 codereview=options.forced_codereview)
3291
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003292 if not cl.GetIssue():
3293 DieWithError('This branch has no associated changelist.')
3294 description = ChangeDescription(cl.GetDescription())
smut@google.com34fb6b12015-07-13 20:03:26 +00003295 if options.display:
tandrii@chromium.org8c3b4422016-04-27 13:11:18 +00003296 print description.description
smut@google.com34fb6b12015-07-13 20:03:26 +00003297 return 0
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003298 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003299 if cl.GetDescription() != description.description:
3300 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003301 return 0
3302
3303
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003304def CreateDescriptionFromLog(args):
3305 """Pulls out the commit log to use as a base for the CL description."""
3306 log_args = []
3307 if len(args) == 1 and not args[0].endswith('.'):
3308 log_args = [args[0] + '..']
3309 elif len(args) == 1 and args[0].endswith('...'):
3310 log_args = [args[0][:-1]]
3311 elif len(args) == 2:
3312 log_args = [args[0] + '..' + args[1]]
3313 else:
3314 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003315 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003316
3317
thestig@chromium.org44202a22014-03-11 19:22:18 +00003318def CMDlint(parser, args):
3319 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003320 parser.add_option('--filter', action='append', metavar='-x,+y',
3321 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003322 auth.add_auth_options(parser)
3323 options, args = parser.parse_args(args)
3324 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003325
3326 # Access to a protected member _XX of a client class
3327 # pylint: disable=W0212
3328 try:
3329 import cpplint
3330 import cpplint_chromium
3331 except ImportError:
3332 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
3333 return 1
3334
3335 # Change the current working directory before calling lint so that it
3336 # shows the correct base.
3337 previous_cwd = os.getcwd()
3338 os.chdir(settings.GetRoot())
3339 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003340 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003341 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3342 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003343 if not files:
3344 print "Cannot lint an empty CL"
3345 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003346
3347 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003348 command = args + files
3349 if options.filter:
3350 command = ['--filter=' + ','.join(options.filter)] + command
3351 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003352
3353 white_regex = re.compile(settings.GetLintRegex())
3354 black_regex = re.compile(settings.GetLintIgnoreRegex())
3355 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3356 for filename in filenames:
3357 if white_regex.match(filename):
3358 if black_regex.match(filename):
3359 print "Ignoring file %s" % filename
3360 else:
3361 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3362 extra_check_functions)
3363 else:
3364 print "Skipping file %s" % filename
3365 finally:
3366 os.chdir(previous_cwd)
3367 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
3368 if cpplint._cpplint_state.error_count != 0:
3369 return 1
3370 return 0
3371
3372
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003373def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003374 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003375 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003376 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003377 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003378 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003379 auth.add_auth_options(parser)
3380 options, args = parser.parse_args(args)
3381 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003382
sbc@chromium.org71437c02015-04-09 19:29:40 +00003383 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00003384 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003385 return 1
3386
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003387 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003388 if args:
3389 base_branch = args[0]
3390 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003391 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003392 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003393
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003394 cl.RunHook(
3395 committing=not options.upload,
3396 may_prompt=False,
3397 verbose=options.verbose,
3398 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003399 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003400
3401
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003402def GenerateGerritChangeId(message):
3403 """Returns Ixxxxxx...xxx change id.
3404
3405 Works the same way as
3406 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3407 but can be called on demand on all platforms.
3408
3409 The basic idea is to generate git hash of a state of the tree, original commit
3410 message, author/committer info and timestamps.
3411 """
3412 lines = []
3413 tree_hash = RunGitSilent(['write-tree'])
3414 lines.append('tree %s' % tree_hash.strip())
3415 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3416 if code == 0:
3417 lines.append('parent %s' % parent.strip())
3418 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3419 lines.append('author %s' % author.strip())
3420 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3421 lines.append('committer %s' % committer.strip())
3422 lines.append('')
3423 # Note: Gerrit's commit-hook actually cleans message of some lines and
3424 # whitespace. This code is not doing this, but it clearly won't decrease
3425 # entropy.
3426 lines.append(message)
3427 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3428 stdin='\n'.join(lines))
3429 return 'I%s' % change_hash.strip()
3430
3431
wittman@chromium.org455dc922015-01-26 20:15:50 +00003432def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3433 """Computes the remote branch ref to use for the CL.
3434
3435 Args:
3436 remote (str): The git remote for the CL.
3437 remote_branch (str): The git remote branch for the CL.
3438 target_branch (str): The target branch specified by the user.
3439 pending_prefix (str): The pending prefix from the settings.
3440 """
3441 if not (remote and remote_branch):
3442 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003443
wittman@chromium.org455dc922015-01-26 20:15:50 +00003444 if target_branch:
3445 # Cannonicalize branch references to the equivalent local full symbolic
3446 # refs, which are then translated into the remote full symbolic refs
3447 # below.
3448 if '/' not in target_branch:
3449 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3450 else:
3451 prefix_replacements = (
3452 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3453 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3454 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3455 )
3456 match = None
3457 for regex, replacement in prefix_replacements:
3458 match = re.search(regex, target_branch)
3459 if match:
3460 remote_branch = target_branch.replace(match.group(0), replacement)
3461 break
3462 if not match:
3463 # This is a branch path but not one we recognize; use as-is.
3464 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003465 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3466 # Handle the refs that need to land in different refs.
3467 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003468
wittman@chromium.org455dc922015-01-26 20:15:50 +00003469 # Create the true path to the remote branch.
3470 # Does the following translation:
3471 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3472 # * refs/remotes/origin/master -> refs/heads/master
3473 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3474 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3475 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3476 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3477 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3478 'refs/heads/')
3479 elif remote_branch.startswith('refs/remotes/branch-heads'):
3480 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3481 # If a pending prefix exists then replace refs/ with it.
3482 if pending_prefix:
3483 remote_branch = remote_branch.replace('refs/', pending_prefix)
3484 return remote_branch
3485
3486
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003487def cleanup_list(l):
3488 """Fixes a list so that comma separated items are put as individual items.
3489
3490 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3491 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3492 """
3493 items = sum((i.split(',') for i in l), [])
3494 stripped_items = (i.strip() for i in items)
3495 return sorted(filter(None, stripped_items))
3496
3497
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003498@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003499def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003500 """Uploads the current changelist to codereview.
3501
3502 Can skip dependency patchset uploads for a branch by running:
3503 git config branch.branch_name.skip-deps-uploads True
3504 To unset run:
3505 git config --unset branch.branch_name.skip-deps-uploads
3506 Can also set the above globally by using the --global flag.
3507 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003508 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3509 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003510 parser.add_option('--bypass-watchlists', action='store_true',
3511 dest='bypass_watchlists',
3512 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003513 parser.add_option('-f', action='store_true', dest='force',
3514 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003515 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003516 parser.add_option('-t', dest='title',
3517 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003518 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003519 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003520 help='reviewer email addresses')
3521 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003522 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003523 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003524 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003525 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003526 parser.add_option('--emulate_svn_auto_props',
3527 '--emulate-svn-auto-props',
3528 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003529 dest="emulate_svn_auto_props",
3530 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003531 parser.add_option('-c', '--use-commit-queue', action='store_true',
3532 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003533 parser.add_option('--private', action='store_true',
3534 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003535 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003536 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003537 metavar='TARGET',
3538 help='Apply CL to remote ref TARGET. ' +
3539 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003540 parser.add_option('--squash', action='store_true',
3541 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003542 parser.add_option('--no-squash', action='store_true',
3543 help='Don\'t squash multiple commits into one ' +
3544 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003545 parser.add_option('--email', default=None,
3546 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003547 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3548 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003549 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3550 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003551 help='Send the patchset to do a CQ dry run right after '
3552 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003553 parser.add_option('--dependencies', action='store_true',
3554 help='Uploads CLs of all the local branches that depend on '
3555 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003556
rmistry@google.com2dd99862015-06-22 12:22:18 +00003557 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003558 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003559 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003560 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003561 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003562 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003563 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003564
sbc@chromium.org71437c02015-04-09 19:29:40 +00003565 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003566 return 1
3567
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003568 options.reviewers = cleanup_list(options.reviewers)
3569 options.cc = cleanup_list(options.cc)
3570
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003571 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3572 settings.GetIsGerrit()
3573
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003574 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00003575 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003576
3577
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003578def IsSubmoduleMergeCommit(ref):
3579 # When submodules are added to the repo, we expect there to be a single
3580 # non-git-svn merge commit at remote HEAD with a signature comment.
3581 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003582 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003583 return RunGit(cmd) != ''
3584
3585
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003586def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003587 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003588
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003589 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3590 upstream and closes the issue automatically and atomically.
3591
3592 Otherwise (in case of Rietveld):
3593 Squashes branch into a single commit.
3594 Updates changelog with metadata (e.g. pointer to review).
3595 Pushes/dcommits the code upstream.
3596 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003597 """
3598 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3599 help='bypass upload presubmit hook')
3600 parser.add_option('-m', dest='message',
3601 help="override review description")
3602 parser.add_option('-f', action='store_true', dest='force',
3603 help="force yes to questions (don't prompt)")
3604 parser.add_option('-c', dest='contributor',
3605 help="external contributor for patch (appended to " +
3606 "description and used as author for git). Should be " +
3607 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003608 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003609 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003610 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003611 auth_config = auth.extract_auth_config_from_options(options)
3612
3613 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003614
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003615 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3616 if cl.IsGerrit():
3617 if options.message:
3618 # This could be implemented, but it requires sending a new patch to
3619 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3620 # Besides, Gerrit has the ability to change the commit message on submit
3621 # automatically, thus there is no need to support this option (so far?).
3622 parser.error('-m MESSAGE option is not supported for Gerrit.')
3623 if options.contributor:
3624 parser.error(
3625 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3626 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3627 'the contributor\'s "name <email>". If you can\'t upload such a '
3628 'commit for review, contact your repository admin and request'
3629 '"Forge-Author" permission.')
3630 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3631 options.verbose)
3632
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003633 current = cl.GetBranch()
3634 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3635 if not settings.GetIsGitSvn() and remote == '.':
3636 print
3637 print 'Attempting to push branch %r into another local branch!' % current
3638 print
3639 print 'Either reparent this branch on top of origin/master:'
3640 print ' git reparent-branch --root'
3641 print
3642 print 'OR run `git rebase-update` if you think the parent branch is already'
3643 print 'committed.'
3644 print
3645 print ' Current parent: %r' % upstream_branch
3646 return 1
3647
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003648 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003649 # Default to merging against our best guess of the upstream branch.
3650 args = [cl.GetUpstreamBranch()]
3651
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003652 if options.contributor:
3653 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
3654 print "Please provide contibutor as 'First Last <email@example.com>'"
3655 return 1
3656
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003657 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003658 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003659
sbc@chromium.org71437c02015-04-09 19:29:40 +00003660 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003661 return 1
3662
3663 # This rev-list syntax means "show all commits not in my branch that
3664 # are in base_branch".
3665 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3666 base_branch]).splitlines()
3667 if upstream_commits:
3668 print ('Base branch "%s" has %d commits '
3669 'not in this branch.' % (base_branch, len(upstream_commits)))
3670 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
3671 return 1
3672
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003673 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003674 svn_head = None
3675 if cmd == 'dcommit' or base_has_submodules:
3676 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
3677 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003678
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003679 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003680 # If the base_head is a submodule merge commit, the first parent of the
3681 # base_head should be a git-svn commit, which is what we're interested in.
3682 base_svn_head = base_branch
3683 if base_has_submodules:
3684 base_svn_head += '^1'
3685
3686 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003687 if extra_commits:
3688 print ('This branch has %d additional commits not upstreamed yet.'
3689 % len(extra_commits.splitlines()))
3690 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
3691 'before attempting to %s.' % (base_branch, cmd))
3692 return 1
3693
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003694 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003695 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003696 author = None
3697 if options.contributor:
3698 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003699 hook_results = cl.RunHook(
3700 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003701 may_prompt=not options.force,
3702 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003703 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003704 if not hook_results.should_continue():
3705 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003706
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003707 # Check the tree status if the tree status URL is set.
3708 status = GetTreeStatus()
3709 if 'closed' == status:
3710 print('The tree is closed. Please wait for it to reopen. Use '
3711 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3712 return 1
3713 elif 'unknown' == status:
3714 print('Unable to determine tree status. Please verify manually and '
3715 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3716 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003717
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003718 change_desc = ChangeDescription(options.message)
3719 if not change_desc.description and cl.GetIssue():
3720 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003721
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003722 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00003723 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003724 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00003725 else:
3726 print 'No description set.'
3727 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
3728 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003729
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003730 # Keep a separate copy for the commit message, because the commit message
3731 # contains the link to the Rietveld issue, while the Rietveld message contains
3732 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003733 # Keep a separate copy for the commit message.
3734 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00003735 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003736
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003737 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00003738 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00003739 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00003740 # after it. Add a period on a new line to circumvent this. Also add a space
3741 # before the period to make sure that Gitiles continues to correctly resolve
3742 # the URL.
3743 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003744 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003745 commit_desc.append_footer('Patch from %s.' % options.contributor)
3746
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00003747 print('Description:')
3748 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003749
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003750 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003751 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00003752 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003753
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003754 # We want to squash all this branch's commits into one commit with the proper
3755 # description. We do this by doing a "reset --soft" to the base branch (which
3756 # keeps the working copy the same), then dcommitting that. If origin/master
3757 # has a submodule merge commit, we'll also need to cherry-pick the squashed
3758 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003759 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003760 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
3761 # Delete the branches if they exist.
3762 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
3763 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
3764 result = RunGitWithCode(showref_cmd)
3765 if result[0] == 0:
3766 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003767
3768 # We might be in a directory that's present in this branch but not in the
3769 # trunk. Move up to the top of the tree so that git commands that expect a
3770 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003771 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003772 if rel_base_path:
3773 os.chdir(rel_base_path)
3774
3775 # Stuff our change into the merge branch.
3776 # We wrap in a try...finally block so if anything goes wrong,
3777 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003778 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003779 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003780 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003781 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003782 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00003783 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003784 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003785 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003786 RunGit(
3787 [
3788 'commit', '--author', options.contributor,
3789 '-m', commit_desc.description,
3790 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003791 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003792 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003793 if base_has_submodules:
3794 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
3795 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
3796 RunGit(['checkout', CHERRY_PICK_BRANCH])
3797 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003798 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003799 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003800 mirror = settings.GetGitMirror(remote)
3801 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003802 pending_prefix = settings.GetPendingRefPrefix()
3803 if not pending_prefix or branch.startswith(pending_prefix):
3804 # If not using refs/pending/heads/* at all, or target ref is already set
3805 # to pending, then push to the target ref directly.
3806 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003807 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003808 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003809 else:
3810 # Cherry-pick the change on top of pending ref and then push it.
3811 assert branch.startswith('refs/'), branch
3812 assert pending_prefix[-1] == '/', pending_prefix
3813 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003814 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003815 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003816 if retcode == 0:
3817 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003818 else:
3819 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003820 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003821 'svn', 'dcommit',
3822 '-C%s' % options.similarity,
3823 '--no-rebase', '--rmdir',
3824 ]
3825 if settings.GetForceHttpsCommitUrl():
3826 # Allow forcing https commit URLs for some projects that don't allow
3827 # committing to http URLs (like Google Code).
3828 remote_url = cl.GetGitSvnRemoteUrl()
3829 if urlparse.urlparse(remote_url).scheme == 'http':
3830 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003831 cmd_args.append('--commit-url=%s' % remote_url)
3832 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003833 if 'Committed r' in output:
3834 revision = re.match(
3835 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
3836 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003837 finally:
3838 # And then swap back to the original branch and clean up.
3839 RunGit(['checkout', '-q', cl.GetBranch()])
3840 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003841 if base_has_submodules:
3842 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003843
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003844 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003845 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003846 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003847
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003848 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003849 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003850 try:
3851 revision = WaitForRealCommit(remote, revision, base_branch, branch)
3852 # We set pushed_to_pending to False, since it made it all the way to the
3853 # real ref.
3854 pushed_to_pending = False
3855 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003856 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003857
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003858 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003859 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003860 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003861 if not to_pending:
3862 if viewvc_url and revision:
3863 change_desc.append_footer(
3864 'Committed: %s%s' % (viewvc_url, revision))
3865 elif revision:
3866 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003867 print ('Closing issue '
3868 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003869 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003870 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003871 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00003872 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00003873 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00003874 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003875 if options.bypass_hooks:
3876 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
3877 else:
3878 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00003879 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003880 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003881
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003882 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003883 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
3884 print 'The commit is in the pending queue (%s).' % pending_ref
3885 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00003886 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003887 'footer.' % branch)
3888
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003889 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
3890 if os.path.isfile(hook):
3891 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003892
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003893 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003894
3895
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003896def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
3897 print
3898 print 'Waiting for commit to be landed on %s...' % real_ref
3899 print '(If you are impatient, you may Ctrl-C once without harm)'
3900 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
3901 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003902 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003903
3904 loop = 0
3905 while True:
3906 sys.stdout.write('fetching (%d)... \r' % loop)
3907 sys.stdout.flush()
3908 loop += 1
3909
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003910 if mirror:
3911 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003912 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
3913 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
3914 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
3915 for commit in commits.splitlines():
3916 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
3917 print 'Found commit on %s' % real_ref
3918 return commit
3919
3920 current_rev = to_rev
3921
3922
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003923def PushToGitPending(remote, pending_ref, upstream_ref):
3924 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
3925
3926 Returns:
3927 (retcode of last operation, output log of last operation).
3928 """
3929 assert pending_ref.startswith('refs/'), pending_ref
3930 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
3931 cherry = RunGit(['rev-parse', 'HEAD']).strip()
3932 code = 0
3933 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003934 max_attempts = 3
3935 attempts_left = max_attempts
3936 while attempts_left:
3937 if attempts_left != max_attempts:
3938 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
3939 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003940
3941 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003942 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003943 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003944 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003945 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003946 print 'Fetch failed with exit code %d.' % code
3947 if out.strip():
3948 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003949 continue
3950
3951 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003952 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003953 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003954 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003955 if code:
3956 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003957 'Your patch doesn\'t apply cleanly to ref \'%s\', '
3958 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003959 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
3960 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003961 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003962 return code, out
3963
3964 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003965 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003966 code, out = RunGitWithCode(
3967 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
3968 if code == 0:
3969 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003970 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003971 return code, out
3972
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003973 print 'Push failed with exit code %d.' % code
3974 if out.strip():
3975 print out.strip()
3976 if IsFatalPushFailure(out):
3977 print (
3978 'Fatal push error. Make sure your .netrc credentials and git '
3979 'user.email are correct and you have push access to the repo.')
3980 return code, out
3981
3982 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003983 return code, out
3984
3985
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003986def IsFatalPushFailure(push_stdout):
3987 """True if retrying push won't help."""
3988 return '(prohibited by Gerrit)' in push_stdout
3989
3990
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003991@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003992def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003993 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003994 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003995 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003996 # If it looks like previous commits were mirrored with git-svn.
3997 message = """This repository appears to be a git-svn mirror, but no
3998upstream SVN master is set. You probably need to run 'git auto-svn' once."""
3999 else:
4000 message = """This doesn't appear to be an SVN repository.
4001If your project has a true, writeable git repository, you probably want to run
4002'git cl land' instead.
4003If your project has a git mirror of an upstream SVN master, you probably need
4004to run 'git svn init'.
4005
4006Using the wrong command might cause your commit to appear to succeed, and the
4007review to be closed, without actually landing upstream. If you choose to
4008proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004009 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004010 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004011 return SendUpstream(parser, args, 'dcommit')
4012
4013
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004014@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004015def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004016 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004017 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004018 print('This appears to be an SVN repository.')
4019 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004020 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004021 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004022 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004023
4024
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004025@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004026def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004027 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004028 parser.add_option('-b', dest='newbranch',
4029 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004030 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004031 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004032 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4033 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004034 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004035 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004036 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004037 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004038 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004039 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004040
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004041
4042 group = optparse.OptionGroup(
4043 parser,
4044 'Options for continuing work on the current issue uploaded from a '
4045 'different clone (e.g. different machine). Must be used independently '
4046 'from the other options. No issue number should be specified, and the '
4047 'branch must have an issue number associated with it')
4048 group.add_option('--reapply', action='store_true', dest='reapply',
4049 help='Reset the branch and reapply the issue.\n'
4050 'CAUTION: This will undo any local changes in this '
4051 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004052
4053 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004054 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004055 parser.add_option_group(group)
4056
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004057 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004058 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004059 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004060 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004061 auth_config = auth.extract_auth_config_from_options(options)
4062
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004063 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004064
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004065 issue_arg = None
4066 if options.reapply :
4067 if len(args) > 0:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004068 parser.error('--reapply implies no additional arguments.')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004069
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004070 issue_arg = cl.GetIssue()
4071 upstream = cl.GetUpstreamBranch()
4072 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004073 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004074
4075 RunGit(['reset', '--hard', upstream])
4076 if options.pull:
4077 RunGit(['pull'])
4078 else:
4079 if len(args) != 1:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004080 parser.error('Must specify issue number or url')
4081 issue_arg = args[0]
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004082
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004083 if not issue_arg:
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004084 parser.print_help()
4085 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004086
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004087 if cl.IsGerrit():
4088 if options.reject:
4089 parser.error('--reject is not supported with Gerrit codereview.')
4090 if options.nocommit:
4091 parser.error('--nocommit is not supported with Gerrit codereview.')
4092 if options.directory:
4093 parser.error('--directory is not supported with Gerrit codereview.')
4094
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004095 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004096 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004097 return 1
4098
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004099 if options.newbranch:
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004100 if options.reapply:
4101 parser.error("--reapply excludes any option other than --pull")
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004102 if options.force:
4103 RunGit(['branch', '-D', options.newbranch],
4104 stderr=subprocess2.PIPE, error_ok=True)
4105 RunGit(['checkout', '-b', options.newbranch,
4106 Changelist().GetUpstreamBranch()])
4107
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004108 return cl.CMDPatchIssue(issue_arg, options.reject, options.nocommit,
4109 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004110
4111
4112def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004113 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004114 # Provide a wrapper for git svn rebase to help avoid accidental
4115 # git svn dcommit.
4116 # It's the only command that doesn't use parser at all since we just defer
4117 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004118
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004119 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004120
4121
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004122def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004123 """Fetches the tree status and returns either 'open', 'closed',
4124 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004125 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004126 if url:
4127 status = urllib2.urlopen(url).read().lower()
4128 if status.find('closed') != -1 or status == '0':
4129 return 'closed'
4130 elif status.find('open') != -1 or status == '1':
4131 return 'open'
4132 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004133 return 'unset'
4134
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004135
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004136def GetTreeStatusReason():
4137 """Fetches the tree status from a json url and returns the message
4138 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004139 url = settings.GetTreeStatusUrl()
4140 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004141 connection = urllib2.urlopen(json_url)
4142 status = json.loads(connection.read())
4143 connection.close()
4144 return status['message']
4145
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004146
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004147def GetBuilderMaster(bot_list):
4148 """For a given builder, fetch the master from AE if available."""
4149 map_url = 'https://builders-map.appspot.com/'
4150 try:
4151 master_map = json.load(urllib2.urlopen(map_url))
4152 except urllib2.URLError as e:
4153 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4154 (map_url, e))
4155 except ValueError as e:
4156 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4157 if not master_map:
4158 return None, 'Failed to build master map.'
4159
4160 result_master = ''
4161 for bot in bot_list:
4162 builder = bot.split(':', 1)[0]
4163 master_list = master_map.get(builder, [])
4164 if not master_list:
4165 return None, ('No matching master for builder %s.' % builder)
4166 elif len(master_list) > 1:
4167 return None, ('The builder name %s exists in multiple masters %s.' %
4168 (builder, master_list))
4169 else:
4170 cur_master = master_list[0]
4171 if not result_master:
4172 result_master = cur_master
4173 elif result_master != cur_master:
4174 return None, 'The builders do not belong to the same master.'
4175 return result_master, None
4176
4177
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004178def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004179 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004180 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004181 status = GetTreeStatus()
4182 if 'unset' == status:
4183 print 'You must configure your tree status URL by running "git cl config".'
4184 return 2
4185
4186 print "The tree is %s" % status
4187 print
4188 print GetTreeStatusReason()
4189 if status != 'open':
4190 return 1
4191 return 0
4192
4193
maruel@chromium.org15192402012-09-06 12:38:29 +00004194def CMDtry(parser, args):
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004195 """Triggers try jobs through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00004196 group = optparse.OptionGroup(parser, "Try job options")
4197 group.add_option(
4198 "-b", "--bot", action="append",
4199 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
4200 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004201 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00004202 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004203 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00004204 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004205 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00004206 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004207 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004208 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004209 "-r", "--revision",
4210 help="Revision to use for the try job; default: the "
4211 "revision will be determined by the try server; see "
4212 "its waterfall for more info")
4213 group.add_option(
4214 "-c", "--clobber", action="store_true", default=False,
4215 help="Force a clobber before building; e.g. don't do an "
4216 "incremental build")
4217 group.add_option(
4218 "--project",
4219 help="Override which project to use. Projects are defined "
4220 "server-side to define what default bot set to use")
4221 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004222 "-p", "--property", dest="properties", action="append", default=[],
4223 help="Specify generic properties in the form -p key1=value1 -p "
4224 "key2=value2 etc (buildbucket only). The value will be treated as "
4225 "json if decodable, or as string otherwise.")
4226 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004227 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004228 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004229 "--use-rietveld", action="store_true", default=False,
4230 help="Use Rietveld to trigger try jobs.")
4231 group.add_option(
4232 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4233 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004234 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004235 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004236 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004237 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004238
machenbach@chromium.org45453142015-09-15 08:45:22 +00004239 if options.use_rietveld and options.properties:
4240 parser.error('Properties can only be specified with buildbucket')
4241
4242 # Make sure that all properties are prop=value pairs.
4243 bad_params = [x for x in options.properties if '=' not in x]
4244 if bad_params:
4245 parser.error('Got properties with missing "=": %s' % bad_params)
4246
maruel@chromium.org15192402012-09-06 12:38:29 +00004247 if args:
4248 parser.error('Unknown arguments: %s' % args)
4249
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004250 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004251 if not cl.GetIssue():
4252 parser.error('Need to upload first')
4253
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004254 if cl.IsGerrit():
4255 parser.error(
4256 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4257 'If your project has Commit Queue, dry run is a workaround:\n'
4258 ' git cl set-commit --dry-run')
4259 # Code below assumes Rietveld issue.
4260 # TODO(tandrii): actually implement for Gerrit http://crbug.com/599931.
4261
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004262 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004263 if props.get('closed'):
4264 parser.error('Cannot send tryjobs for a closed CL')
4265
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004266 if props.get('private'):
4267 parser.error('Cannot use trybots with private issue')
4268
maruel@chromium.org15192402012-09-06 12:38:29 +00004269 if not options.name:
4270 options.name = cl.GetBranch()
4271
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004272 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004273 options.master, err_msg = GetBuilderMaster(options.bot)
4274 if err_msg:
4275 parser.error('Tryserver master cannot be found because: %s\n'
4276 'Please manually specify the tryserver master'
4277 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004278
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004279 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004280 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004281 if not options.bot:
4282 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004283
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004284 # Get try masters from PRESUBMIT.py files.
4285 masters = presubmit_support.DoGetTryMasters(
4286 change,
4287 change.LocalPaths(),
4288 settings.GetRoot(),
4289 None,
4290 None,
4291 options.verbose,
4292 sys.stdout)
4293 if masters:
4294 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004295
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004296 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4297 options.bot = presubmit_support.DoGetTrySlaves(
4298 change,
4299 change.LocalPaths(),
4300 settings.GetRoot(),
4301 None,
4302 None,
4303 options.verbose,
4304 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004305
4306 if not options.bot:
4307 # Get try masters from cq.cfg if any.
4308 # TODO(tandrii): some (but very few) projects store cq.cfg in different
4309 # location.
4310 cq_cfg = os.path.join(change.RepositoryRoot(),
4311 'infra', 'config', 'cq.cfg')
4312 if os.path.exists(cq_cfg):
4313 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00004314 cq_masters = commit_queue.get_master_builder_map(
4315 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004316 for master, builders in cq_masters.iteritems():
4317 for builder in builders:
4318 # Skip presubmit builders, because these will fail without LGTM.
machenbach@chromium.orgc2dfcb82016-04-29 12:21:36 +00004319 if 'presubmit' not in builder.lower():
4320 masters.setdefault(master, {})[builder] = ['defaulttests']
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004321 if masters:
4322 return masters
4323
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004324 if not options.bot:
4325 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00004326
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004327 builders_and_tests = {}
4328 # TODO(machenbach): The old style command-line options don't support
4329 # multiple try masters yet.
4330 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4331 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4332
4333 for bot in old_style:
4334 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004335 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004336 elif ',' in bot:
4337 parser.error('Specify one bot per --bot flag')
4338 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004339 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004340
4341 for bot, tests in new_style:
4342 builders_and_tests.setdefault(bot, []).extend(tests)
4343
4344 # Return a master map with one master to be backwards compatible. The
4345 # master name defaults to an empty string, which will cause the master
4346 # not to be set on rietveld (deprecated).
4347 return {options.master: builders_and_tests}
4348
4349 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004350
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004351 for builders in masters.itervalues():
4352 if any('triggered' in b for b in builders):
4353 print >> sys.stderr, (
4354 'ERROR You are trying to send a job to a triggered bot. This type of'
4355 ' bot requires an\ninitial job from a parent (usually a builder). '
4356 'Instead send your job to the parent.\n'
4357 'Bot list: %s' % builders)
4358 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004359
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004360 patchset = cl.GetMostRecentPatchset()
4361 if patchset and patchset != cl.GetPatchset():
4362 print(
4363 '\nWARNING Mismatch between local config and server. Did a previous '
4364 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4365 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004366 if options.luci:
4367 trigger_luci_job(cl, masters, options)
4368 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004369 try:
4370 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4371 except BuildbucketResponseException as ex:
4372 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004373 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004374 except Exception as e:
4375 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4376 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
4377 e, stacktrace)
4378 return 1
4379 else:
4380 try:
4381 cl.RpcServer().trigger_distributed_try_jobs(
4382 cl.GetIssue(), patchset, options.name, options.clobber,
4383 options.revision, masters)
4384 except urllib2.HTTPError as e:
4385 if e.code == 404:
4386 print('404 from rietveld; '
4387 'did you mean to use "git try" instead of "git cl try"?')
4388 return 1
4389 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004390
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004391 for (master, builders) in sorted(masters.iteritems()):
4392 if master:
4393 print 'Master: %s' % master
4394 length = max(len(builder) for builder in builders)
4395 for builder in sorted(builders):
4396 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00004397 return 0
4398
4399
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004400def CMDtry_results(parser, args):
4401 group = optparse.OptionGroup(parser, "Try job results options")
4402 group.add_option(
4403 "-p", "--patchset", type=int, help="patchset number if not current.")
4404 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004405 "--print-master", action='store_true', help="print master name as well.")
4406 group.add_option(
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004407 "--color", action='store_true', default=setup_color.IS_TTY,
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004408 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004409 group.add_option(
4410 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4411 help="Host of buildbucket. The default host is %default.")
4412 parser.add_option_group(group)
4413 auth.add_auth_options(parser)
4414 options, args = parser.parse_args(args)
4415 if args:
4416 parser.error('Unrecognized args: %s' % ' '.join(args))
4417
4418 auth_config = auth.extract_auth_config_from_options(options)
4419 cl = Changelist(auth_config=auth_config)
4420 if not cl.GetIssue():
4421 parser.error('Need to upload first')
4422
4423 if not options.patchset:
4424 options.patchset = cl.GetMostRecentPatchset()
4425 if options.patchset and options.patchset != cl.GetPatchset():
4426 print(
4427 '\nWARNING Mismatch between local config and server. Did a previous '
4428 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4429 'Continuing using\npatchset %s.\n' % options.patchset)
4430 try:
4431 jobs = fetch_try_jobs(auth_config, cl, options)
4432 except BuildbucketResponseException as ex:
4433 print 'Buildbucket error: %s' % ex
4434 return 1
4435 except Exception as e:
4436 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4437 print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
4438 e, stacktrace)
4439 return 1
4440 print_tryjobs(options, jobs)
4441 return 0
4442
4443
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004444@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004445def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004446 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004447 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004448 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004449 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004450
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004451 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004452 if args:
4453 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004454 branch = cl.GetBranch()
4455 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004456 cl = Changelist()
4457 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004458
4459 # Clear configured merge-base, if there is one.
4460 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004461 else:
4462 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004463 return 0
4464
4465
thestig@chromium.org00858c82013-12-02 23:08:03 +00004466def CMDweb(parser, args):
4467 """Opens the current CL in the web browser."""
4468 _, args = parser.parse_args(args)
4469 if args:
4470 parser.error('Unrecognized args: %s' % ' '.join(args))
4471
4472 issue_url = Changelist().GetIssueURL()
4473 if not issue_url:
4474 print >> sys.stderr, 'ERROR No issue to open'
4475 return 1
4476
4477 webbrowser.open(issue_url)
4478 return 0
4479
4480
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004481def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004482 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004483 parser.add_option('-d', '--dry-run', action='store_true',
4484 help='trigger in dry run mode')
4485 parser.add_option('-c', '--clear', action='store_true',
4486 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004487 auth.add_auth_options(parser)
4488 options, args = parser.parse_args(args)
4489 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004490 if args:
4491 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004492 if options.dry_run and options.clear:
4493 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4494
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004495 cl = Changelist(auth_config=auth_config)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004496 if options.clear:
4497 state = _CQState.CLEAR
4498 elif options.dry_run:
4499 state = _CQState.DRY_RUN
4500 else:
4501 state = _CQState.COMMIT
4502 if not cl.GetIssue():
4503 parser.error('Must upload the issue first')
4504 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004505 return 0
4506
4507
groby@chromium.org411034a2013-02-26 15:12:01 +00004508def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004509 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004510 auth.add_auth_options(parser)
4511 options, args = parser.parse_args(args)
4512 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004513 if args:
4514 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004515 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00004516 # Ensure there actually is an issue to close.
4517 cl.GetDescription()
4518 cl.CloseIssue()
4519 return 0
4520
4521
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004522def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004523 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004524 auth.add_auth_options(parser)
4525 options, args = parser.parse_args(args)
4526 auth_config = auth.extract_auth_config_from_options(options)
4527 if args:
4528 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004529
4530 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004531 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004532 # Staged changes would be committed along with the patch from last
4533 # upload, hence counted toward the "last upload" side in the final
4534 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004535 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004536 return 1
4537
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004538 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004539 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004540 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004541 if not issue:
4542 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004543 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004544 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004545
4546 # Create a new branch based on the merge-base
4547 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004548 # Clear cached branch in cl object, to avoid overwriting original CL branch
4549 # properties.
4550 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004551 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004552 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004553 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004554 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004555 return rtn
4556
wychen@chromium.org06928532015-02-03 02:11:29 +00004557 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004558 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00004559 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004560 finally:
4561 RunGit(['checkout', '-q', branch])
4562 RunGit(['branch', '-D', TMP_BRANCH])
4563
4564 return 0
4565
4566
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004567def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004568 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004569 parser.add_option(
4570 '--no-color',
4571 action='store_true',
4572 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004573 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004574 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004575 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004576
4577 author = RunGit(['config', 'user.email']).strip() or None
4578
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004579 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004580
4581 if args:
4582 if len(args) > 1:
4583 parser.error('Unknown args')
4584 base_branch = args[0]
4585 else:
4586 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004587 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004588
4589 change = cl.GetChange(base_branch, None)
4590 return owners_finder.OwnersFinder(
4591 [f.LocalPath() for f in
4592 cl.GetChange(base_branch, None).AffectedFiles()],
4593 change.RepositoryRoot(), author,
4594 fopen=file, os_path=os.path, glob=glob.glob,
4595 disable_color=options.no_color).run()
4596
4597
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004598def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004599 """Generates a diff command."""
4600 # Generate diff for the current branch's changes.
4601 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4602 upstream_commit, '--' ]
4603
4604 if args:
4605 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004606 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004607 diff_cmd.append(arg)
4608 else:
4609 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004610
4611 return diff_cmd
4612
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004613def MatchingFileType(file_name, extensions):
4614 """Returns true if the file name ends with one of the given extensions."""
4615 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004616
enne@chromium.org555cfe42014-01-29 18:21:39 +00004617@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004618def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004619 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00004620 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004621 GN_EXTS = ['.gn', '.gni']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004622 parser.add_option('--full', action='store_true',
4623 help='Reformat the full content of all touched files')
4624 parser.add_option('--dry-run', action='store_true',
4625 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004626 parser.add_option('--python', action='store_true',
4627 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004628 parser.add_option('--diff', action='store_true',
4629 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004630 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004631
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004632 # git diff generates paths against the root of the repository. Change
4633 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004634 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004635 if rel_base_path:
4636 os.chdir(rel_base_path)
4637
digit@chromium.org29e47272013-05-17 17:01:46 +00004638 # Grab the merge-base commit, i.e. the upstream commit of the current
4639 # branch when it was created or the last time it was rebased. This is
4640 # to cover the case where the user may have called "git fetch origin",
4641 # moving the origin branch to a newer commit, but hasn't rebased yet.
4642 upstream_commit = None
4643 cl = Changelist()
4644 upstream_branch = cl.GetUpstreamBranch()
4645 if upstream_branch:
4646 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4647 upstream_commit = upstream_commit.strip()
4648
4649 if not upstream_commit:
4650 DieWithError('Could not find base commit for this branch. '
4651 'Are you in detached state?')
4652
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004653 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4654 diff_output = RunGit(changed_files_cmd)
4655 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004656 # Filter out files deleted by this CL
4657 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004658
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004659 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4660 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4661 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004662 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004663
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004664 top_dir = os.path.normpath(
4665 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
4666
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004667 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4668 # formatted. This is used to block during the presubmit.
4669 return_value = 0
4670
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004671 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004672 # Locate the clang-format binary in the checkout
4673 try:
4674 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
4675 except clang_format.NotFoundError, e:
4676 DieWithError(e)
4677
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004678 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004679 cmd = [clang_format_tool]
4680 if not opts.dry_run and not opts.diff:
4681 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004682 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004683 if opts.diff:
4684 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004685 else:
4686 env = os.environ.copy()
4687 env['PATH'] = str(os.path.dirname(clang_format_tool))
4688 try:
4689 script = clang_format.FindClangFormatScriptInChromiumTree(
4690 'clang-format-diff.py')
4691 except clang_format.NotFoundError, e:
4692 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004693
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004694 cmd = [sys.executable, script, '-p0']
4695 if not opts.dry_run and not opts.diff:
4696 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004697
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004698 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
4699 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004700
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004701 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
4702 if opts.diff:
4703 sys.stdout.write(stdout)
4704 if opts.dry_run and len(stdout) > 0:
4705 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004706
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004707 # Similar code to above, but using yapf on .py files rather than clang-format
4708 # on C/C++ files
4709 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004710 yapf_tool = gclient_utils.FindExecutable('yapf')
4711 if yapf_tool is None:
4712 DieWithError('yapf not found in PATH')
4713
4714 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004715 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004716 cmd = [yapf_tool]
4717 if not opts.dry_run and not opts.diff:
4718 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004719 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004720 if opts.diff:
4721 sys.stdout.write(stdout)
4722 else:
4723 # TODO(sbc): yapf --lines mode still has some issues.
4724 # https://github.com/google/yapf/issues/154
4725 DieWithError('--python currently only works with --full')
4726
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004727 # Dart's formatter does not have the nice property of only operating on
4728 # modified chunks, so hard code full.
4729 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004730 try:
4731 command = [dart_format.FindDartFmtToolInChromiumTree()]
4732 if not opts.dry_run and not opts.diff:
4733 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004734 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004735
ppi@chromium.org6593d932016-03-03 15:41:15 +00004736 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004737 if opts.dry_run and stdout:
4738 return_value = 2
4739 except dart_format.NotFoundError as e:
erikcorry@chromium.org3e445022015-12-17 09:07:26 +00004740 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
4741 'found in this checkout. Files in other languages are still ' +
4742 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004743
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004744 # Format GN build files. Always run on full build files for canonical form.
4745 if gn_diff_files:
4746 cmd = ['gn', 'format']
4747 if not opts.dry_run and not opts.diff:
4748 cmd.append('--in-place')
4749 for gn_diff_file in gn_diff_files:
bsep@chromium.org627d9002016-04-29 00:00:52 +00004750 stdout = RunCommand(cmd + [gn_diff_file],
4751 shell=sys.platform == 'win32',
4752 cwd=top_dir)
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004753 if opts.diff:
4754 sys.stdout.write(stdout)
4755
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004756 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004757
4758
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004759@subcommand.usage('<codereview url or issue id>')
4760def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004761 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004762 _, args = parser.parse_args(args)
4763
4764 if len(args) != 1:
4765 parser.print_help()
4766 return 1
4767
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004768 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00004769 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004770 parser.print_help()
4771 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00004772 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004773
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004774 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00004775 output = RunGit(['config', '--local', '--get-regexp',
4776 r'branch\..*\.%s' % issueprefix],
4777 error_ok=True)
4778 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004779 if issue == target_issue:
4780 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004781
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004782 branches = []
4783 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00004784 branches.extend(find_issues(cls.IssueSettingSuffix()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004785 if len(branches) == 0:
4786 print 'No branch found for issue %s.' % target_issue
4787 return 1
4788 if len(branches) == 1:
4789 RunGit(['checkout', branches[0]])
4790 else:
4791 print 'Multiple branches match issue %s:' % target_issue
4792 for i in range(len(branches)):
4793 print '%d: %s' % (i, branches[i])
4794 which = raw_input('Choose by index: ')
4795 try:
4796 RunGit(['checkout', branches[int(which)]])
4797 except (IndexError, ValueError):
4798 print 'Invalid selection, not checking out any branch.'
4799 return 1
4800
4801 return 0
4802
4803
maruel@chromium.org29404b52014-09-08 22:58:00 +00004804def CMDlol(parser, args):
4805 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00004806 print zlib.decompress(base64.b64decode(
4807 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
4808 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
4809 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
4810 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00004811 return 0
4812
4813
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004814class OptionParser(optparse.OptionParser):
4815 """Creates the option parse and add --verbose support."""
4816 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004817 optparse.OptionParser.__init__(
4818 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004819 self.add_option(
4820 '-v', '--verbose', action='count', default=0,
4821 help='Use 2 times for more debugging info')
4822
4823 def parse_args(self, args=None, values=None):
4824 options, args = optparse.OptionParser.parse_args(self, args, values)
4825 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
4826 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
4827 return options, args
4828
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004829
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004830def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00004831 if sys.hexversion < 0x02060000:
4832 print >> sys.stderr, (
4833 '\nYour python version %s is unsupported, please upgrade.\n' %
4834 sys.version.split(' ', 1)[0])
4835 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004836
maruel@chromium.orgddd59412011-11-30 14:20:38 +00004837 # Reload settings.
4838 global settings
4839 settings = Settings()
4840
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004841 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004842 dispatcher = subcommand.CommandDispatcher(__name__)
4843 try:
4844 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00004845 except auth.AuthenticationError as e:
4846 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004847 except urllib2.HTTPError, e:
4848 if e.code != 500:
4849 raise
4850 DieWithError(
4851 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
4852 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00004853 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004854
4855
4856if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004857 # These affect sys.stdout so do it outside of main() to simplify mocks in
4858 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00004859 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004860 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00004861 try:
4862 sys.exit(main(sys.argv[1:]))
4863 except KeyboardInterrupt:
4864 sys.stderr.write('interrupted\n')
4865 sys.exit(1)