blob: 35bd37e5e43d475a17ae5aee01e824c7564ba47d [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00008"""A git-command for integrating reviews on Rietveld and Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000010from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000011from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000012import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000013import collections
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000014import glob
sheyang@google.com6ebaf782015-05-12 19:17:54 +000015import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000016import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000018import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import optparse
20import os
tandrii@chromium.org04ea8462016-04-25 19:51:21 +000021import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000023import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024import sys
tandrii@chromium.org04ea8462016-04-25 19:51:21 +000025import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000026import textwrap
sheyang@google.com6ebaf782015-05-12 19:17:54 +000027import time
28import traceback
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000029import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000030import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000031import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000032import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000033import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000034import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000035
36try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000037 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000038except ImportError:
39 pass
40
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000041from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000042from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000043from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000044import auth
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +000045from luci_hacks import trigger_luci_job as luci_trigger
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000046import clang_format
tandrii@chromium.org71184c02016-01-13 15:18:44 +000047import commit_queue
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000048import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000049import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000050import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000051import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000052import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000053import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000054import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000055import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000056import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000057import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000058import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000059import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000060import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000061import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000062import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000063import watchlists
64
maruel@chromium.org0633fb42013-08-16 20:06:14 +000065__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000066
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000067DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000068POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000069DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000070GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
rmistry@google.comc68112d2015-03-03 12:48:06 +000071REFS_THAT_ALIAS_TO_OTHER_REFS = {
72 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
73 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
74}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000075
thestig@chromium.org44202a22014-03-11 19:22:18 +000076# Valid extensions for files we want to lint.
77DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
78DEFAULT_LINT_IGNORE_REGEX = r"$^"
79
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000080# Shortcut since it quickly becomes redundant.
81Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000082
maruel@chromium.orgddd59412011-11-30 14:20:38 +000083# Initialized in main()
84settings = None
85
86
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000087def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000088 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000089 sys.exit(1)
90
91
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000092def GetNoGitPagerEnv():
93 env = os.environ.copy()
94 # 'cat' is a magical git string that disables pagers on all platforms.
95 env['GIT_PAGER'] = 'cat'
96 return env
97
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000098
bsep@chromium.org627d9002016-04-29 00:00:52 +000099def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000100 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000101 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000102 except subprocess2.CalledProcessError as e:
103 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000104 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000105 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000106 'Command "%s" failed.\n%s' % (
107 ' '.join(args), error_message or e.stdout or ''))
108 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000109
110
111def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000112 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000113 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000114
115
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000116def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000117 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000118 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000119 if suppress_stderr:
120 stderr = subprocess2.VOID
121 else:
122 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000123 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000124 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000125 stdout=subprocess2.PIPE,
126 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000127 return code, out[0]
128 except ValueError:
129 # When the subprocess fails, it returns None. That triggers a ValueError
130 # when trying to unpack the return value into (out, code).
131 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000132
133
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000134def RunGitSilent(args):
135 """Returns stdout, suppresses stderr and ingores the return code."""
136 return RunGitWithCode(args, suppress_stderr=True)[1]
137
138
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000139def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000140 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000141 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000142 return (version.startswith(prefix) and
143 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000144
145
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000146def BranchExists(branch):
147 """Return True if specified branch exists."""
148 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
149 suppress_stderr=True)
150 return not code
151
152
maruel@chromium.org90541732011-04-01 17:54:18 +0000153def ask_for_data(prompt):
154 try:
155 return raw_input(prompt)
156 except KeyboardInterrupt:
157 # Hide the exception.
158 sys.exit(1)
159
160
iannucci@chromium.org79540052012-10-19 23:15:26 +0000161def git_set_branch_value(key, value):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000162 branch = GetCurrentBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000163 if not branch:
164 return
165
166 cmd = ['config']
167 if isinstance(value, int):
168 cmd.append('--int')
169 git_key = 'branch.%s.%s' % (branch, key)
170 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000171
172
173def git_get_branch_default(key, default):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000174 branch = GetCurrentBranch()
iannucci@chromium.org79540052012-10-19 23:15:26 +0000175 if branch:
176 git_key = 'branch.%s.%s' % (branch, key)
177 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
178 try:
179 return int(stdout.strip())
180 except ValueError:
181 pass
182 return default
183
184
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000185def add_git_similarity(parser):
186 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000187 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000188 help='Sets the percentage that a pair of files need to match in order to'
189 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000190 parser.add_option(
191 '--find-copies', action='store_true',
192 help='Allows git to look for copies.')
193 parser.add_option(
194 '--no-find-copies', action='store_false', dest='find_copies',
195 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000196
197 old_parser_args = parser.parse_args
198 def Parse(args):
199 options, args = old_parser_args(args)
200
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000201 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000202 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000203 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000204 print('Note: Saving similarity of %d%% in git config.'
205 % options.similarity)
206 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000207
iannucci@chromium.org79540052012-10-19 23:15:26 +0000208 options.similarity = max(0, min(options.similarity, 100))
209
210 if options.find_copies is None:
211 options.find_copies = bool(
212 git_get_branch_default('git-find-copies', True))
213 else:
214 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000215
216 print('Using %d%% similarity for rename/copy detection. '
217 'Override with --similarity.' % options.similarity)
218
219 return options, args
220 parser.parse_args = Parse
221
222
machenbach@chromium.org45453142015-09-15 08:45:22 +0000223def _get_properties_from_options(options):
224 properties = dict(x.split('=', 1) for x in options.properties)
225 for key, val in properties.iteritems():
226 try:
227 properties[key] = json.loads(val)
228 except ValueError:
229 pass # If a value couldn't be evaluated, treat it as a string.
230 return properties
231
232
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000233def _prefix_master(master):
234 """Convert user-specified master name to full master name.
235
236 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
237 name, while the developers always use shortened master name
238 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
239 function does the conversion for buildbucket migration.
240 """
241 prefix = 'master.'
242 if master.startswith(prefix):
243 return master
244 return '%s%s' % (prefix, master)
245
246
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000247def _buildbucket_retry(operation_name, http, *args, **kwargs):
248 """Retries requests to buildbucket service and returns parsed json content."""
249 try_count = 0
250 while True:
251 response, content = http.request(*args, **kwargs)
252 try:
253 content_json = json.loads(content)
254 except ValueError:
255 content_json = None
256
257 # Buildbucket could return an error even if status==200.
258 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000259 error = content_json.get('error')
260 if error.get('code') == 403:
261 raise BuildbucketResponseException(
262 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000263 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000264 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000265 raise BuildbucketResponseException(msg)
266
267 if response.status == 200:
268 if not content_json:
269 raise BuildbucketResponseException(
270 'Buildbucket returns invalid json content: %s.\n'
271 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
272 content)
273 return content_json
274 if response.status < 500 or try_count >= 2:
275 raise httplib2.HttpLib2Error(content)
276
277 # status >= 500 means transient failures.
278 logging.debug('Transient errors when %s. Will retry.', operation_name)
279 time.sleep(0.5 + 1.5*try_count)
280 try_count += 1
281 assert False, 'unreachable'
282
283
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000284def trigger_luci_job(changelist, masters, options):
285 """Send a job to run on LUCI."""
286 issue_props = changelist.GetIssueProperties()
287 issue = changelist.GetIssue()
288 patchset = changelist.GetMostRecentPatchset()
289 for builders_and_tests in sorted(masters.itervalues()):
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000290 # TODO(hinoka et al): add support for other properties.
291 # Currently, this completely ignores testfilter and other properties.
292 for builder in sorted(builders_and_tests):
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000293 luci_trigger.trigger(
294 builder, 'HEAD', issue, patchset, issue_props['project'])
295
296
machenbach@chromium.org45453142015-09-15 08:45:22 +0000297def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000298 rietveld_url = settings.GetDefaultServerUrl()
299 rietveld_host = urlparse.urlparse(rietveld_url).hostname
300 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
301 http = authenticator.authorize(httplib2.Http())
302 http.force_exception_to_status_code = True
303 issue_props = changelist.GetIssueProperties()
304 issue = changelist.GetIssue()
305 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000306 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000307
308 buildbucket_put_url = (
309 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000310 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000311 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
312 hostname=rietveld_host,
313 issue=issue,
314 patch=patchset)
315
316 batch_req_body = {'builds': []}
317 print_text = []
318 print_text.append('Tried jobs on:')
319 for master, builders_and_tests in sorted(masters.iteritems()):
320 print_text.append('Master: %s' % master)
321 bucket = _prefix_master(master)
322 for builder, tests in sorted(builders_and_tests.iteritems()):
323 print_text.append(' %s: %s' % (builder, tests))
324 parameters = {
325 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000326 'changes': [{
327 'author': {'email': issue_props['owner_email']},
328 'revision': options.revision,
329 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000330 'properties': {
331 'category': category,
332 'issue': issue,
333 'master': master,
334 'patch_project': issue_props['project'],
335 'patch_storage': 'rietveld',
336 'patchset': patchset,
337 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000338 'rietveld': rietveld_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000339 },
340 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000341 if 'presubmit' in builder.lower():
342 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000343 if tests:
344 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000345 if properties:
346 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000347 if options.clobber:
348 parameters['properties']['clobber'] = True
349 batch_req_body['builds'].append(
350 {
351 'bucket': bucket,
352 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000353 'client_operation_id': str(uuid.uuid4()),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000354 'tags': ['builder:%s' % builder,
355 'buildset:%s' % buildset,
356 'master:%s' % master,
357 'user_agent:git_cl_try']
358 }
359 )
360
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000361 _buildbucket_retry(
362 'triggering tryjobs',
363 http,
364 buildbucket_put_url,
365 'PUT',
366 body=json.dumps(batch_req_body),
367 headers={'Content-Type': 'application/json'}
368 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000369 print_text.append('To see results here, run: git cl try-results')
370 print_text.append('To see results in browser, run: git cl web')
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000371 print '\n'.join(print_text)
kjellander@chromium.org44424542015-06-02 18:35:29 +0000372
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000373
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000374def fetch_try_jobs(auth_config, changelist, options):
375 """Fetches tryjobs from buildbucket.
376
377 Returns a map from build id to build info as json dictionary.
378 """
379 rietveld_url = settings.GetDefaultServerUrl()
380 rietveld_host = urlparse.urlparse(rietveld_url).hostname
381 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
382 if authenticator.has_cached_credentials():
383 http = authenticator.authorize(httplib2.Http())
384 else:
385 print ('Warning: Some results might be missing because %s' %
386 # Get the message on how to login.
387 auth.LoginRequiredError(rietveld_host).message)
388 http = httplib2.Http()
389
390 http.force_exception_to_status_code = True
391
392 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
393 hostname=rietveld_host,
394 issue=changelist.GetIssue(),
395 patch=options.patchset)
396 params = {'tag': 'buildset:%s' % buildset}
397
398 builds = {}
399 while True:
400 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
401 hostname=options.buildbucket_host,
402 params=urllib.urlencode(params))
403 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
404 for build in content.get('builds', []):
405 builds[build['id']] = build
406 if 'next_cursor' in content:
407 params['start_cursor'] = content['next_cursor']
408 else:
409 break
410 return builds
411
412
413def print_tryjobs(options, builds):
414 """Prints nicely result of fetch_try_jobs."""
415 if not builds:
416 print 'No tryjobs scheduled'
417 return
418
419 # Make a copy, because we'll be modifying builds dictionary.
420 builds = builds.copy()
421 builder_names_cache = {}
422
423 def get_builder(b):
424 try:
425 return builder_names_cache[b['id']]
426 except KeyError:
427 try:
428 parameters = json.loads(b['parameters_json'])
429 name = parameters['builder_name']
430 except (ValueError, KeyError) as error:
431 print 'WARNING: failed to get builder name for build %s: %s' % (
432 b['id'], error)
433 name = None
434 builder_names_cache[b['id']] = name
435 return name
436
437 def get_bucket(b):
438 bucket = b['bucket']
439 if bucket.startswith('master.'):
440 return bucket[len('master.'):]
441 return bucket
442
443 if options.print_master:
444 name_fmt = '%%-%ds %%-%ds' % (
445 max(len(str(get_bucket(b))) for b in builds.itervalues()),
446 max(len(str(get_builder(b))) for b in builds.itervalues()))
447 def get_name(b):
448 return name_fmt % (get_bucket(b), get_builder(b))
449 else:
450 name_fmt = '%%-%ds' % (
451 max(len(str(get_builder(b))) for b in builds.itervalues()))
452 def get_name(b):
453 return name_fmt % get_builder(b)
454
455 def sort_key(b):
456 return b['status'], b.get('result'), get_name(b), b.get('url')
457
458 def pop(title, f, color=None, **kwargs):
459 """Pop matching builds from `builds` dict and print them."""
460
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000461 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000462 colorize = str
463 else:
464 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
465
466 result = []
467 for b in builds.values():
468 if all(b.get(k) == v for k, v in kwargs.iteritems()):
469 builds.pop(b['id'])
470 result.append(b)
471 if result:
472 print colorize(title)
473 for b in sorted(result, key=sort_key):
474 print ' ', colorize('\t'.join(map(str, f(b))))
475
476 total = len(builds)
477 pop(status='COMPLETED', result='SUCCESS',
478 title='Successes:', color=Fore.GREEN,
479 f=lambda b: (get_name(b), b.get('url')))
480 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
481 title='Infra Failures:', color=Fore.MAGENTA,
482 f=lambda b: (get_name(b), b.get('url')))
483 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
484 title='Failures:', color=Fore.RED,
485 f=lambda b: (get_name(b), b.get('url')))
486 pop(status='COMPLETED', result='CANCELED',
487 title='Canceled:', color=Fore.MAGENTA,
488 f=lambda b: (get_name(b),))
489 pop(status='COMPLETED', result='FAILURE',
490 failure_reason='INVALID_BUILD_DEFINITION',
491 title='Wrong master/builder name:', color=Fore.MAGENTA,
492 f=lambda b: (get_name(b),))
493 pop(status='COMPLETED', result='FAILURE',
494 title='Other failures:',
495 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
496 pop(status='COMPLETED',
497 title='Other finished:',
498 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
499 pop(status='STARTED',
500 title='Started:', color=Fore.YELLOW,
501 f=lambda b: (get_name(b), b.get('url')))
502 pop(status='SCHEDULED',
503 title='Scheduled:',
504 f=lambda b: (get_name(b), 'id=%s' % b['id']))
505 # The last section is just in case buildbucket API changes OR there is a bug.
506 pop(title='Other:',
507 f=lambda b: (get_name(b), 'id=%s' % b['id']))
508 assert len(builds) == 0
509 print 'Total: %d tryjobs' % total
510
511
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000512def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
513 """Return the corresponding git ref if |base_url| together with |glob_spec|
514 matches the full |url|.
515
516 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
517 """
518 fetch_suburl, as_ref = glob_spec.split(':')
519 if allow_wildcards:
520 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
521 if glob_match:
522 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
523 # "branches/{472,597,648}/src:refs/remotes/svn/*".
524 branch_re = re.escape(base_url)
525 if glob_match.group(1):
526 branch_re += '/' + re.escape(glob_match.group(1))
527 wildcard = glob_match.group(2)
528 if wildcard == '*':
529 branch_re += '([^/]*)'
530 else:
531 # Escape and replace surrounding braces with parentheses and commas
532 # with pipe symbols.
533 wildcard = re.escape(wildcard)
534 wildcard = re.sub('^\\\\{', '(', wildcard)
535 wildcard = re.sub('\\\\,', '|', wildcard)
536 wildcard = re.sub('\\\\}$', ')', wildcard)
537 branch_re += wildcard
538 if glob_match.group(3):
539 branch_re += re.escape(glob_match.group(3))
540 match = re.match(branch_re, url)
541 if match:
542 return re.sub('\*$', match.group(1), as_ref)
543
544 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
545 if fetch_suburl:
546 full_url = base_url + '/' + fetch_suburl
547 else:
548 full_url = base_url
549 if full_url == url:
550 return as_ref
551 return None
552
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000553
iannucci@chromium.org79540052012-10-19 23:15:26 +0000554def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000555 """Prints statistics about the change to the user."""
556 # --no-ext-diff is broken in some versions of Git, so try to work around
557 # this by overriding the environment (but there is still a problem if the
558 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000559 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000560 if 'GIT_EXTERNAL_DIFF' in env:
561 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000562
563 if find_copies:
564 similarity_options = ['--find-copies-harder', '-l100000',
565 '-C%s' % similarity]
566 else:
567 similarity_options = ['-M%s' % similarity]
568
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000569 try:
570 stdout = sys.stdout.fileno()
571 except AttributeError:
572 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000573 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000574 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000575 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000576 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000577
578
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000579class BuildbucketResponseException(Exception):
580 pass
581
582
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000583class Settings(object):
584 def __init__(self):
585 self.default_server = None
586 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000587 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000588 self.is_git_svn = None
589 self.svn_branch = None
590 self.tree_status_url = None
591 self.viewvc_url = None
592 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000593 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000594 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000595 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000596 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000597 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000598 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000599 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000600
601 def LazyUpdateIfNeeded(self):
602 """Updates the settings from a codereview.settings file, if available."""
603 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000604 # The only value that actually changes the behavior is
605 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000606 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000607 error_ok=True
608 ).strip().lower()
609
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000610 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000611 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000612 LoadCodereviewSettingsFromFile(cr_settings_file)
613 self.updated = True
614
615 def GetDefaultServerUrl(self, error_ok=False):
616 if not self.default_server:
617 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000618 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000619 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000620 if error_ok:
621 return self.default_server
622 if not self.default_server:
623 error_message = ('Could not find settings file. You must configure '
624 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000625 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000626 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000627 return self.default_server
628
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000629 @staticmethod
630 def GetRelativeRoot():
631 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000632
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000633 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000634 if self.root is None:
635 self.root = os.path.abspath(self.GetRelativeRoot())
636 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000637
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000638 def GetGitMirror(self, remote='origin'):
639 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000640 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000641 if not os.path.isdir(local_url):
642 return None
643 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
644 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
645 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
646 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
647 if mirror.exists():
648 return mirror
649 return None
650
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000651 def GetIsGitSvn(self):
652 """Return true if this repo looks like it's using git-svn."""
653 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000654 if self.GetPendingRefPrefix():
655 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
656 self.is_git_svn = False
657 else:
658 # If you have any "svn-remote.*" config keys, we think you're using svn.
659 self.is_git_svn = RunGitWithCode(
660 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000661 return self.is_git_svn
662
663 def GetSVNBranch(self):
664 if self.svn_branch is None:
665 if not self.GetIsGitSvn():
666 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
667
668 # Try to figure out which remote branch we're based on.
669 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000670 # 1) iterate through our branch history and find the svn URL.
671 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000672
673 # regexp matching the git-svn line that contains the URL.
674 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
675
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000676 # We don't want to go through all of history, so read a line from the
677 # pipe at a time.
678 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000679 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000680 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
681 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000682 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000683 for line in proc.stdout:
684 match = git_svn_re.match(line)
685 if match:
686 url = match.group(1)
687 proc.stdout.close() # Cut pipe.
688 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000689
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000690 if url:
691 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
692 remotes = RunGit(['config', '--get-regexp',
693 r'^svn-remote\..*\.url']).splitlines()
694 for remote in remotes:
695 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000696 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000697 remote = match.group(1)
698 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000699 rewrite_root = RunGit(
700 ['config', 'svn-remote.%s.rewriteRoot' % remote],
701 error_ok=True).strip()
702 if rewrite_root:
703 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000704 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000705 ['config', 'svn-remote.%s.fetch' % remote],
706 error_ok=True).strip()
707 if fetch_spec:
708 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
709 if self.svn_branch:
710 break
711 branch_spec = RunGit(
712 ['config', 'svn-remote.%s.branches' % remote],
713 error_ok=True).strip()
714 if branch_spec:
715 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
716 if self.svn_branch:
717 break
718 tag_spec = RunGit(
719 ['config', 'svn-remote.%s.tags' % remote],
720 error_ok=True).strip()
721 if tag_spec:
722 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
723 if self.svn_branch:
724 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000725
726 if not self.svn_branch:
727 DieWithError('Can\'t guess svn branch -- try specifying it on the '
728 'command line')
729
730 return self.svn_branch
731
732 def GetTreeStatusUrl(self, error_ok=False):
733 if not self.tree_status_url:
734 error_message = ('You must configure your tree status URL by running '
735 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000736 self.tree_status_url = self._GetRietveldConfig(
737 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000738 return self.tree_status_url
739
740 def GetViewVCUrl(self):
741 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000742 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000743 return self.viewvc_url
744
rmistry@google.com90752582014-01-14 21:04:50 +0000745 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000746 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000747
rmistry@google.com78948ed2015-07-08 23:09:57 +0000748 def GetIsSkipDependencyUpload(self, branch_name):
749 """Returns true if specified branch should skip dep uploads."""
750 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
751 error_ok=True)
752
rmistry@google.com5626a922015-02-26 14:03:30 +0000753 def GetRunPostUploadHook(self):
754 run_post_upload_hook = self._GetRietveldConfig(
755 'run-post-upload-hook', error_ok=True)
756 return run_post_upload_hook == "True"
757
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000758 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000759 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000760
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000761 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000762 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000763
ukai@chromium.orge8077812012-02-03 03:41:46 +0000764 def GetIsGerrit(self):
765 """Return true if this repo is assosiated with gerrit code review system."""
766 if self.is_gerrit is None:
767 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
768 return self.is_gerrit
769
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000770 def GetSquashGerritUploads(self):
771 """Return true if uploads to Gerrit should be squashed by default."""
772 if self.squash_gerrit_uploads is None:
773 self.squash_gerrit_uploads = (
774 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
775 error_ok=True).strip() == 'true')
776 return self.squash_gerrit_uploads
777
tandrii@chromium.org28253532016-04-14 13:46:56 +0000778 def GetGerritSkipEnsureAuthenticated(self):
779 """Return True if EnsureAuthenticated should not be done for Gerrit
780 uploads."""
781 if self.gerrit_skip_ensure_authenticated is None:
782 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000783 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000784 error_ok=True).strip() == 'true')
785 return self.gerrit_skip_ensure_authenticated
786
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000787 def GetGitEditor(self):
788 """Return the editor specified in the git config, or None if none is."""
789 if self.git_editor is None:
790 self.git_editor = self._GetConfig('core.editor', error_ok=True)
791 return self.git_editor or None
792
thestig@chromium.org44202a22014-03-11 19:22:18 +0000793 def GetLintRegex(self):
794 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
795 DEFAULT_LINT_REGEX)
796
797 def GetLintIgnoreRegex(self):
798 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
799 DEFAULT_LINT_IGNORE_REGEX)
800
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000801 def GetProject(self):
802 if not self.project:
803 self.project = self._GetRietveldConfig('project', error_ok=True)
804 return self.project
805
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000806 def GetForceHttpsCommitUrl(self):
807 if not self.force_https_commit_url:
808 self.force_https_commit_url = self._GetRietveldConfig(
809 'force-https-commit-url', error_ok=True)
810 return self.force_https_commit_url
811
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000812 def GetPendingRefPrefix(self):
813 if not self.pending_ref_prefix:
814 self.pending_ref_prefix = self._GetRietveldConfig(
815 'pending-ref-prefix', error_ok=True)
816 return self.pending_ref_prefix
817
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000818 def _GetRietveldConfig(self, param, **kwargs):
819 return self._GetConfig('rietveld.' + param, **kwargs)
820
rmistry@google.com78948ed2015-07-08 23:09:57 +0000821 def _GetBranchConfig(self, branch_name, param, **kwargs):
822 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
823
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000824 def _GetConfig(self, param, **kwargs):
825 self.LazyUpdateIfNeeded()
826 return RunGit(['config', param], **kwargs).strip()
827
828
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000829def ShortBranchName(branch):
830 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000831 return branch.replace('refs/heads/', '', 1)
832
833
834def GetCurrentBranchRef():
835 """Returns branch ref (e.g., refs/heads/master) or None."""
836 return RunGit(['symbolic-ref', 'HEAD'],
837 stderr=subprocess2.VOID, error_ok=True).strip() or None
838
839
840def GetCurrentBranch():
841 """Returns current branch or None.
842
843 For refs/heads/* branches, returns just last part. For others, full ref.
844 """
845 branchref = GetCurrentBranchRef()
846 if branchref:
847 return ShortBranchName(branchref)
848 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000849
850
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000851class _CQState(object):
852 """Enum for states of CL with respect to Commit Queue."""
853 NONE = 'none'
854 DRY_RUN = 'dry_run'
855 COMMIT = 'commit'
856
857 ALL_STATES = [NONE, DRY_RUN, COMMIT]
858
859
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000860class _ParsedIssueNumberArgument(object):
861 def __init__(self, issue=None, patchset=None, hostname=None):
862 self.issue = issue
863 self.patchset = patchset
864 self.hostname = hostname
865
866 @property
867 def valid(self):
868 return self.issue is not None
869
870
871class _RietveldParsedIssueNumberArgument(_ParsedIssueNumberArgument):
872 def __init__(self, *args, **kwargs):
873 self.patch_url = kwargs.pop('patch_url', None)
874 super(_RietveldParsedIssueNumberArgument, self).__init__(*args, **kwargs)
875
876
877def ParseIssueNumberArgument(arg):
878 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
879 fail_result = _ParsedIssueNumberArgument()
880
881 if arg.isdigit():
882 return _ParsedIssueNumberArgument(issue=int(arg))
883 if not arg.startswith('http'):
884 return fail_result
885 url = gclient_utils.UpgradeToHttps(arg)
886 try:
887 parsed_url = urlparse.urlparse(url)
888 except ValueError:
889 return fail_result
890 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
891 tmp = cls.ParseIssueURL(parsed_url)
892 if tmp is not None:
893 return tmp
894 return fail_result
895
896
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000897class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000898 """Changelist works with one changelist in local branch.
899
900 Supports two codereview backends: Rietveld or Gerrit, selected at object
901 creation.
902
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +0000903 Notes:
904 * Not safe for concurrent multi-{thread,process} use.
905 * Caches values from current branch. Therefore, re-use after branch change
906 with care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000907 """
908
909 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
910 """Create a new ChangeList instance.
911
912 If issue is given, the codereview must be given too.
913
914 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
915 Otherwise, it's decided based on current configuration of the local branch,
916 with default being 'rietveld' for backwards compatibility.
917 See _load_codereview_impl for more details.
918
919 **kwargs will be passed directly to codereview implementation.
920 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000921 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000922 global settings
923 if not settings:
924 # Happens when git_cl.py is used as a utility library.
925 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000926
927 if issue:
928 assert codereview, 'codereview must be known, if issue is known'
929
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000930 self.branchref = branchref
931 if self.branchref:
932 self.branch = ShortBranchName(self.branchref)
933 else:
934 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000935 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000936 self.lookedup_issue = False
937 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000938 self.has_description = False
939 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000940 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000941 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000942 self.cc = None
943 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000944 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000945
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000946 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000947 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000948 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000949 assert self._codereview_impl
950 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000951
952 def _load_codereview_impl(self, codereview=None, **kwargs):
953 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000954 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
955 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
956 self._codereview = codereview
957 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000958 return
959
960 # Automatic selection based on issue number set for a current branch.
961 # Rietveld takes precedence over Gerrit.
962 assert not self.issue
963 # Whether we find issue or not, we are doing the lookup.
964 self.lookedup_issue = True
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000965 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000966 setting = cls.IssueSetting(self.GetBranch())
967 issue = RunGit(['config', setting], error_ok=True).strip()
968 if issue:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000969 self._codereview = codereview
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000970 self._codereview_impl = cls(self, **kwargs)
971 self.issue = int(issue)
972 return
973
974 # No issue is set for this branch, so decide based on repo-wide settings.
975 return self._load_codereview_impl(
976 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
977 **kwargs)
978
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000979 def IsGerrit(self):
980 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000981
982 def GetCCList(self):
983 """Return the users cc'd on this CL.
984
985 Return is a string suitable for passing to gcl with the --cc flag.
986 """
987 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000988 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000989 more_cc = ','.join(self.watchers)
990 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
991 return self.cc
992
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000993 def GetCCListWithoutDefault(self):
994 """Return the users cc'd on this CL excluding default ones."""
995 if self.cc is None:
996 self.cc = ','.join(self.watchers)
997 return self.cc
998
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000999 def SetWatchers(self, watchers):
1000 """Set the list of email addresses that should be cc'd based on the changed
1001 files in this CL.
1002 """
1003 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001004
1005 def GetBranch(self):
1006 """Returns the short branch name, e.g. 'master'."""
1007 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001008 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001009 if not branchref:
1010 return None
1011 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001012 self.branch = ShortBranchName(self.branchref)
1013 return self.branch
1014
1015 def GetBranchRef(self):
1016 """Returns the full branch name, e.g. 'refs/heads/master'."""
1017 self.GetBranch() # Poke the lazy loader.
1018 return self.branchref
1019
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001020 def ClearBranch(self):
1021 """Clears cached branch data of this object."""
1022 self.branch = self.branchref = None
1023
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001024 @staticmethod
1025 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001026 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001027 e.g. 'origin', 'refs/heads/master'
1028 """
1029 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001030 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
1031 error_ok=True).strip()
1032 if upstream_branch:
1033 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
1034 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001035 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1036 error_ok=True).strip()
1037 if upstream_branch:
1038 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001039 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001040 # Fall back on trying a git-svn upstream branch.
1041 if settings.GetIsGitSvn():
1042 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001043 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001044 # Else, try to guess the origin remote.
1045 remote_branches = RunGit(['branch', '-r']).split()
1046 if 'origin/master' in remote_branches:
1047 # Fall back on origin/master if it exits.
1048 remote = 'origin'
1049 upstream_branch = 'refs/heads/master'
1050 elif 'origin/trunk' in remote_branches:
1051 # Fall back on origin/trunk if it exists. Generally a shared
1052 # git-svn clone
1053 remote = 'origin'
1054 upstream_branch = 'refs/heads/trunk'
1055 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001056 DieWithError(
1057 'Unable to determine default branch to diff against.\n'
1058 'Either pass complete "git diff"-style arguments, like\n'
1059 ' git cl upload origin/master\n'
1060 'or verify this branch is set up to track another \n'
1061 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001062
1063 return remote, upstream_branch
1064
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001065 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001066 upstream_branch = self.GetUpstreamBranch()
1067 if not BranchExists(upstream_branch):
1068 DieWithError('The upstream for the current branch (%s) does not exist '
1069 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001070 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001071 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001072
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001073 def GetUpstreamBranch(self):
1074 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001075 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001076 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001077 upstream_branch = upstream_branch.replace('refs/heads/',
1078 'refs/remotes/%s/' % remote)
1079 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1080 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001081 self.upstream_branch = upstream_branch
1082 return self.upstream_branch
1083
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001084 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001085 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001086 remote, branch = None, self.GetBranch()
1087 seen_branches = set()
1088 while branch not in seen_branches:
1089 seen_branches.add(branch)
1090 remote, branch = self.FetchUpstreamTuple(branch)
1091 branch = ShortBranchName(branch)
1092 if remote != '.' or branch.startswith('refs/remotes'):
1093 break
1094 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001095 remotes = RunGit(['remote'], error_ok=True).split()
1096 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001097 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001098 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001099 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001100 logging.warning('Could not determine which remote this change is '
1101 'associated with, so defaulting to "%s". This may '
1102 'not be what you want. You may prevent this message '
1103 'by running "git svn info" as documented here: %s',
1104 self._remote,
1105 GIT_INSTRUCTIONS_URL)
1106 else:
1107 logging.warn('Could not determine which remote this change is '
1108 'associated with. You may prevent this message by '
1109 'running "git svn info" as documented here: %s',
1110 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001111 branch = 'HEAD'
1112 if branch.startswith('refs/remotes'):
1113 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001114 elif branch.startswith('refs/branch-heads/'):
1115 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001116 else:
1117 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001118 return self._remote
1119
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001120 def GitSanityChecks(self, upstream_git_obj):
1121 """Checks git repo status and ensures diff is from local commits."""
1122
sbc@chromium.org79706062015-01-14 21:18:12 +00001123 if upstream_git_obj is None:
1124 if self.GetBranch() is None:
1125 print >> sys.stderr, (
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00001126 'ERROR: unable to determine current branch (detached HEAD?)')
sbc@chromium.org79706062015-01-14 21:18:12 +00001127 else:
1128 print >> sys.stderr, (
1129 'ERROR: no upstream branch')
1130 return False
1131
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001132 # Verify the commit we're diffing against is in our current branch.
1133 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1134 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1135 if upstream_sha != common_ancestor:
1136 print >> sys.stderr, (
1137 'ERROR: %s is not in the current branch. You may need to rebase '
1138 'your tracking branch' % upstream_sha)
1139 return False
1140
1141 # List the commits inside the diff, and verify they are all local.
1142 commits_in_diff = RunGit(
1143 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1144 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1145 remote_branch = remote_branch.strip()
1146 if code != 0:
1147 _, remote_branch = self.GetRemoteBranch()
1148
1149 commits_in_remote = RunGit(
1150 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1151
1152 common_commits = set(commits_in_diff) & set(commits_in_remote)
1153 if common_commits:
1154 print >> sys.stderr, (
1155 'ERROR: Your diff contains %d commits already in %s.\n'
1156 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1157 'the diff. If you are using a custom git flow, you can override'
1158 ' the reference used for this check with "git config '
1159 'gitcl.remotebranch <git-ref>".' % (
1160 len(common_commits), remote_branch, upstream_git_obj))
1161 return False
1162 return True
1163
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001164 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001165 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001166
1167 Returns None if it is not set.
1168 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001169 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
1170 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001171
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001172 def GetGitSvnRemoteUrl(self):
1173 """Return the configured git-svn remote URL parsed from git svn info.
1174
1175 Returns None if it is not set.
1176 """
1177 # URL is dependent on the current directory.
1178 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1179 if data:
1180 keys = dict(line.split(': ', 1) for line in data.splitlines()
1181 if ': ' in line)
1182 return keys.get('URL', None)
1183 return None
1184
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001185 def GetRemoteUrl(self):
1186 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1187
1188 Returns None if there is no remote.
1189 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001190 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001191 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1192
1193 # If URL is pointing to a local directory, it is probably a git cache.
1194 if os.path.isdir(url):
1195 url = RunGit(['config', 'remote.%s.url' % remote],
1196 error_ok=True,
1197 cwd=url).strip()
1198 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001199
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001200 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001201 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001202 if self.issue is None and not self.lookedup_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001203 issue = RunGit(['config',
1204 self._codereview_impl.IssueSetting(self.GetBranch())],
1205 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001206 self.issue = int(issue) or None if issue else None
1207 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001208 return self.issue
1209
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001210 def GetIssueURL(self):
1211 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001212 issue = self.GetIssue()
1213 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001214 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001215 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001216
1217 def GetDescription(self, pretty=False):
1218 if not self.has_description:
1219 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001220 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001221 self.has_description = True
1222 if pretty:
1223 wrapper = textwrap.TextWrapper()
1224 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1225 return wrapper.fill(self.description)
1226 return self.description
1227
1228 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001229 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001230 if self.patchset is None and not self.lookedup_patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001231 patchset = RunGit(['config', self._codereview_impl.PatchsetSetting()],
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001232 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001233 self.patchset = int(patchset) or None if patchset else None
1234 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001235 return self.patchset
1236
1237 def SetPatchset(self, patchset):
1238 """Set this branch's patchset. If patchset=0, clears the patchset."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001239 patchset_setting = self._codereview_impl.PatchsetSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001240 if patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001241 RunGit(['config', patchset_setting, str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001242 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001243 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001244 RunGit(['config', '--unset', patchset_setting],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001245 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001246 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001247
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001248 def SetIssue(self, issue=None):
1249 """Set this branch's issue. If issue isn't given, clears the issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001250 issue_setting = self._codereview_impl.IssueSetting(self.GetBranch())
1251 codereview_setting = self._codereview_impl.GetCodereviewServerSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001252 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001253 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001254 RunGit(['config', issue_setting, str(issue)])
1255 codereview_server = self._codereview_impl.GetCodereviewServer()
1256 if codereview_server:
1257 RunGit(['config', codereview_setting, codereview_server])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001258 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +00001259 current_issue = self.GetIssue()
1260 if current_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001261 RunGit(['config', '--unset', issue_setting])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001262 self.issue = None
1263 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001264
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001265 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001266 if not self.GitSanityChecks(upstream_branch):
1267 DieWithError('\nGit sanity check failure')
1268
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001269 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001270 if not root:
1271 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001272 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001273
1274 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001275 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001276 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001277 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001278 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001279 except subprocess2.CalledProcessError:
1280 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001281 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001282 'This branch probably doesn\'t exist anymore. To reset the\n'
1283 'tracking branch, please run\n'
1284 ' git branch --set-upstream %s trunk\n'
1285 'replacing trunk with origin/master or the relevant branch') %
1286 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001287
maruel@chromium.org52424302012-08-29 15:14:30 +00001288 issue = self.GetIssue()
1289 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001290 if issue:
1291 description = self.GetDescription()
1292 else:
1293 # If the change was never uploaded, use the log messages of all commits
1294 # up to the branch point, as git cl upload will prefill the description
1295 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001296 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1297 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001298
1299 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001300 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001301 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001302 name,
1303 description,
1304 absroot,
1305 files,
1306 issue,
1307 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001308 author,
1309 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001310
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001311 def UpdateDescription(self, description):
1312 self.description = description
1313 return self._codereview_impl.UpdateDescriptionRemote(description)
1314
1315 def RunHook(self, committing, may_prompt, verbose, change):
1316 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1317 try:
1318 return presubmit_support.DoPresubmitChecks(change, committing,
1319 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1320 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001321 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1322 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001323 except presubmit_support.PresubmitFailure, e:
1324 DieWithError(
1325 ('%s\nMaybe your depot_tools is out of date?\n'
1326 'If all fails, contact maruel@') % e)
1327
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001328 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1329 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001330 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1331 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001332 else:
1333 # Assume url.
1334 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1335 urlparse.urlparse(issue_arg))
1336 if not parsed_issue_arg or not parsed_issue_arg.valid:
1337 DieWithError('Failed to parse issue argument "%s". '
1338 'Must be an issue number or a valid URL.' % issue_arg)
1339 return self._codereview_impl.CMDPatchWithParsedIssue(
1340 parsed_issue_arg, reject, nocommit, directory)
1341
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001342 def CMDUpload(self, options, git_diff_args, orig_args):
1343 """Uploads a change to codereview."""
1344 if git_diff_args:
1345 # TODO(ukai): is it ok for gerrit case?
1346 base_branch = git_diff_args[0]
1347 else:
1348 if self.GetBranch() is None:
1349 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1350
1351 # Default to diffing against common ancestor of upstream branch
1352 base_branch = self.GetCommonAncestorWithUpstream()
1353 git_diff_args = [base_branch, 'HEAD']
1354
1355 # Make sure authenticated to codereview before running potentially expensive
1356 # hooks. It is a fast, best efforts check. Codereview still can reject the
1357 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001358 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001359
1360 # Apply watchlists on upload.
1361 change = self.GetChange(base_branch, None)
1362 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1363 files = [f.LocalPath() for f in change.AffectedFiles()]
1364 if not options.bypass_watchlists:
1365 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1366
1367 if not options.bypass_hooks:
1368 if options.reviewers or options.tbr_owners:
1369 # Set the reviewer list now so that presubmit checks can access it.
1370 change_description = ChangeDescription(change.FullDescriptionText())
1371 change_description.update_reviewers(options.reviewers,
1372 options.tbr_owners,
1373 change)
1374 change.SetDescriptionText(change_description.description)
1375 hook_results = self.RunHook(committing=False,
1376 may_prompt=not options.force,
1377 verbose=options.verbose,
1378 change=change)
1379 if not hook_results.should_continue():
1380 return 1
1381 if not options.reviewers and hook_results.reviewers:
1382 options.reviewers = hook_results.reviewers.split(',')
1383
1384 if self.GetIssue():
1385 latest_patchset = self.GetMostRecentPatchset()
1386 local_patchset = self.GetPatchset()
1387 if (latest_patchset and local_patchset and
1388 local_patchset != latest_patchset):
1389 print ('The last upload made from this repository was patchset #%d but '
1390 'the most recent patchset on the server is #%d.'
1391 % (local_patchset, latest_patchset))
1392 print ('Uploading will still work, but if you\'ve uploaded to this '
1393 'issue from another machine or branch the patch you\'re '
1394 'uploading now might not include those changes.')
1395 ask_for_data('About to upload; enter to confirm.')
1396
1397 print_stats(options.similarity, options.find_copies, git_diff_args)
1398 ret = self.CMDUploadChange(options, git_diff_args, change)
1399 if not ret:
1400 git_set_branch_value('last-upload-hash',
1401 RunGit(['rev-parse', 'HEAD']).strip())
1402 # Run post upload hooks, if specified.
1403 if settings.GetRunPostUploadHook():
1404 presubmit_support.DoPostUploadExecuter(
1405 change,
1406 self,
1407 settings.GetRoot(),
1408 options.verbose,
1409 sys.stdout)
1410
1411 # Upload all dependencies if specified.
1412 if options.dependencies:
1413 print
1414 print '--dependencies has been specified.'
1415 print 'All dependent local branches will be re-uploaded.'
1416 print
1417 # Remove the dependencies flag from args so that we do not end up in a
1418 # loop.
1419 orig_args.remove('--dependencies')
1420 ret = upload_branch_deps(self, orig_args)
1421 return ret
1422
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001423 def SetCQState(self, new_state):
1424 """Update the CQ state for latest patchset.
1425
1426 Issue must have been already uploaded and known.
1427 """
1428 assert new_state in _CQState.ALL_STATES
1429 assert self.GetIssue()
1430 return self._codereview_impl.SetCQState(new_state)
1431
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001432 # Forward methods to codereview specific implementation.
1433
1434 def CloseIssue(self):
1435 return self._codereview_impl.CloseIssue()
1436
1437 def GetStatus(self):
1438 return self._codereview_impl.GetStatus()
1439
1440 def GetCodereviewServer(self):
1441 return self._codereview_impl.GetCodereviewServer()
1442
1443 def GetApprovingReviewers(self):
1444 return self._codereview_impl.GetApprovingReviewers()
1445
1446 def GetMostRecentPatchset(self):
1447 return self._codereview_impl.GetMostRecentPatchset()
1448
1449 def __getattr__(self, attr):
1450 # This is because lots of untested code accesses Rietveld-specific stuff
1451 # directly, and it's hard to fix for sure. So, just let it work, and fix
1452 # on a cases by case basis.
1453 return getattr(self._codereview_impl, attr)
1454
1455
1456class _ChangelistCodereviewBase(object):
1457 """Abstract base class encapsulating codereview specifics of a changelist."""
1458 def __init__(self, changelist):
1459 self._changelist = changelist # instance of Changelist
1460
1461 def __getattr__(self, attr):
1462 # Forward methods to changelist.
1463 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1464 # _RietveldChangelistImpl to avoid this hack?
1465 return getattr(self._changelist, attr)
1466
1467 def GetStatus(self):
1468 """Apply a rough heuristic to give a simple summary of an issue's review
1469 or CQ status, assuming adherence to a common workflow.
1470
1471 Returns None if no issue for this branch, or specific string keywords.
1472 """
1473 raise NotImplementedError()
1474
1475 def GetCodereviewServer(self):
1476 """Returns server URL without end slash, like "https://codereview.com"."""
1477 raise NotImplementedError()
1478
1479 def FetchDescription(self):
1480 """Fetches and returns description from the codereview server."""
1481 raise NotImplementedError()
1482
1483 def GetCodereviewServerSetting(self):
1484 """Returns git config setting for the codereview server."""
1485 raise NotImplementedError()
1486
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001487 @classmethod
1488 def IssueSetting(cls, branch):
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001489 return 'branch.%s.%s' % (branch, cls.IssueSettingSuffix())
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001490
1491 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001492 def IssueSettingSuffix(cls):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001493 """Returns name of git config setting which stores issue number for a given
1494 branch."""
1495 raise NotImplementedError()
1496
1497 def PatchsetSetting(self):
1498 """Returns name of git config setting which stores issue number."""
1499 raise NotImplementedError()
1500
1501 def GetRieveldObjForPresubmit(self):
1502 # This is an unfortunate Rietveld-embeddedness in presubmit.
1503 # For non-Rietveld codereviews, this probably should return a dummy object.
1504 raise NotImplementedError()
1505
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001506 def GetGerritObjForPresubmit(self):
1507 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1508 return None
1509
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001510 def UpdateDescriptionRemote(self, description):
1511 """Update the description on codereview site."""
1512 raise NotImplementedError()
1513
1514 def CloseIssue(self):
1515 """Closes the issue."""
1516 raise NotImplementedError()
1517
1518 def GetApprovingReviewers(self):
1519 """Returns a list of reviewers approving the change.
1520
1521 Note: not necessarily committers.
1522 """
1523 raise NotImplementedError()
1524
1525 def GetMostRecentPatchset(self):
1526 """Returns the most recent patchset number from the codereview site."""
1527 raise NotImplementedError()
1528
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001529 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1530 directory):
1531 """Fetches and applies the issue.
1532
1533 Arguments:
1534 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1535 reject: if True, reject the failed patch instead of switching to 3-way
1536 merge. Rietveld only.
1537 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1538 only.
1539 directory: switch to directory before applying the patch. Rietveld only.
1540 """
1541 raise NotImplementedError()
1542
1543 @staticmethod
1544 def ParseIssueURL(parsed_url):
1545 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1546 failed."""
1547 raise NotImplementedError()
1548
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001549 def EnsureAuthenticated(self, force):
1550 """Best effort check that user is authenticated with codereview server.
1551
1552 Arguments:
1553 force: whether to skip confirmation questions.
1554 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001555 raise NotImplementedError()
1556
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001557 def CMDUploadChange(self, options, args, change):
1558 """Uploads a change to codereview."""
1559 raise NotImplementedError()
1560
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001561 def SetCQState(self, new_state):
1562 """Update the CQ state for latest patchset.
1563
1564 Issue must have been already uploaded and known.
1565 """
1566 raise NotImplementedError()
1567
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001568
1569class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1570 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1571 super(_RietveldChangelistImpl, self).__init__(changelist)
1572 assert settings, 'must be initialized in _ChangelistCodereviewBase'
1573 settings.GetDefaultServerUrl()
1574
1575 self._rietveld_server = rietveld_server
1576 self._auth_config = auth_config
1577 self._props = None
1578 self._rpc_server = None
1579
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001580 def GetCodereviewServer(self):
1581 if not self._rietveld_server:
1582 # If we're on a branch then get the server potentially associated
1583 # with that branch.
1584 if self.GetIssue():
1585 rietveld_server_setting = self.GetCodereviewServerSetting()
1586 if rietveld_server_setting:
1587 self._rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1588 ['config', rietveld_server_setting], error_ok=True).strip())
1589 if not self._rietveld_server:
1590 self._rietveld_server = settings.GetDefaultServerUrl()
1591 return self._rietveld_server
1592
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001593 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001594 """Best effort check that user is authenticated with Rietveld server."""
1595 if self._auth_config.use_oauth2:
1596 authenticator = auth.get_authenticator_for_host(
1597 self.GetCodereviewServer(), self._auth_config)
1598 if not authenticator.has_cached_credentials():
1599 raise auth.LoginRequiredError(self.GetCodereviewServer())
1600
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001601 def FetchDescription(self):
1602 issue = self.GetIssue()
1603 assert issue
1604 try:
1605 return self.RpcServer().get_description(issue).strip()
1606 except urllib2.HTTPError as e:
1607 if e.code == 404:
1608 DieWithError(
1609 ('\nWhile fetching the description for issue %d, received a '
1610 '404 (not found)\n'
1611 'error. It is likely that you deleted this '
1612 'issue on the server. If this is the\n'
1613 'case, please run\n\n'
1614 ' git cl issue 0\n\n'
1615 'to clear the association with the deleted issue. Then run '
1616 'this command again.') % issue)
1617 else:
1618 DieWithError(
1619 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1620 except urllib2.URLError as e:
1621 print >> sys.stderr, (
1622 'Warning: Failed to retrieve CL description due to network '
1623 'failure.')
1624 return ''
1625
1626 def GetMostRecentPatchset(self):
1627 return self.GetIssueProperties()['patchsets'][-1]
1628
1629 def GetPatchSetDiff(self, issue, patchset):
1630 return self.RpcServer().get(
1631 '/download/issue%s_%s.diff' % (issue, patchset))
1632
1633 def GetIssueProperties(self):
1634 if self._props is None:
1635 issue = self.GetIssue()
1636 if not issue:
1637 self._props = {}
1638 else:
1639 self._props = self.RpcServer().get_issue_properties(issue, True)
1640 return self._props
1641
1642 def GetApprovingReviewers(self):
1643 return get_approving_reviewers(self.GetIssueProperties())
1644
1645 def AddComment(self, message):
1646 return self.RpcServer().add_comment(self.GetIssue(), message)
1647
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001648 def GetStatus(self):
1649 """Apply a rough heuristic to give a simple summary of an issue's review
1650 or CQ status, assuming adherence to a common workflow.
1651
1652 Returns None if no issue for this branch, or one of the following keywords:
1653 * 'error' - error from review tool (including deleted issues)
1654 * 'unsent' - not sent for review
1655 * 'waiting' - waiting for review
1656 * 'reply' - waiting for owner to reply to review
1657 * 'lgtm' - LGTM from at least one approved reviewer
1658 * 'commit' - in the commit queue
1659 * 'closed' - closed
1660 """
1661 if not self.GetIssue():
1662 return None
1663
1664 try:
1665 props = self.GetIssueProperties()
1666 except urllib2.HTTPError:
1667 return 'error'
1668
1669 if props.get('closed'):
1670 # Issue is closed.
1671 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001672 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001673 # Issue is in the commit queue.
1674 return 'commit'
1675
1676 try:
1677 reviewers = self.GetApprovingReviewers()
1678 except urllib2.HTTPError:
1679 return 'error'
1680
1681 if reviewers:
1682 # Was LGTM'ed.
1683 return 'lgtm'
1684
1685 messages = props.get('messages') or []
1686
1687 if not messages:
1688 # No message was sent.
1689 return 'unsent'
1690 if messages[-1]['sender'] != props.get('owner_email'):
1691 # Non-LGTM reply from non-owner
1692 return 'reply'
1693 return 'waiting'
1694
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001695 def UpdateDescriptionRemote(self, description):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001696 return self.RpcServer().update_description(
1697 self.GetIssue(), self.description)
1698
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001699 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001700 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001701
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001702 def SetFlag(self, flag, value):
1703 """Patchset must match."""
1704 if not self.GetPatchset():
1705 DieWithError('The patchset needs to match. Send another patchset.')
1706 try:
1707 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001708 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001709 except urllib2.HTTPError, e:
1710 if e.code == 404:
1711 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1712 if e.code == 403:
1713 DieWithError(
1714 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1715 'match?') % (self.GetIssue(), self.GetPatchset()))
1716 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001717
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001718 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001719 """Returns an upload.RpcServer() to access this review's rietveld instance.
1720 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001721 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001722 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001723 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001724 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001725 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001726
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001727 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001728 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001729 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001730
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001731 def PatchsetSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001732 """Return the git setting that stores this change's most recent patchset."""
1733 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1734
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001735 def GetCodereviewServerSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001736 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001737 branch = self.GetBranch()
1738 if branch:
1739 return 'branch.%s.rietveldserver' % branch
1740 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001741
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001742 def GetRieveldObjForPresubmit(self):
1743 return self.RpcServer()
1744
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001745 def SetCQState(self, new_state):
1746 props = self.GetIssueProperties()
1747 if props.get('private'):
1748 DieWithError('Cannot set-commit on private issue')
1749
1750 if new_state == _CQState.COMMIT:
1751 self.SetFlag('commit', '1')
1752 elif new_state == _CQState.NONE:
1753 self.SetFlag('commit', '0')
1754 else:
1755 raise NotImplementedError()
1756
1757
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001758 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1759 directory):
1760 # TODO(maruel): Use apply_issue.py
1761
1762 # PatchIssue should never be called with a dirty tree. It is up to the
1763 # caller to check this, but just in case we assert here since the
1764 # consequences of the caller not checking this could be dire.
1765 assert(not git_common.is_dirty_git_tree('apply'))
1766 assert(parsed_issue_arg.valid)
1767 self._changelist.issue = parsed_issue_arg.issue
1768 if parsed_issue_arg.hostname:
1769 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1770
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001771 if (isinstance(parsed_issue_arg, _RietveldParsedIssueNumberArgument) and
1772 parsed_issue_arg.patch_url):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001773 assert parsed_issue_arg.patchset
1774 patchset = parsed_issue_arg.patchset
1775 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1776 else:
1777 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1778 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1779
1780 # Switch up to the top-level directory, if necessary, in preparation for
1781 # applying the patch.
1782 top = settings.GetRelativeRoot()
1783 if top:
1784 os.chdir(top)
1785
1786 # Git patches have a/ at the beginning of source paths. We strip that out
1787 # with a sed script rather than the -p flag to patch so we can feed either
1788 # Git or svn-style patches into the same apply command.
1789 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1790 try:
1791 patch_data = subprocess2.check_output(
1792 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1793 except subprocess2.CalledProcessError:
1794 DieWithError('Git patch mungling failed.')
1795 logging.info(patch_data)
1796
1797 # We use "git apply" to apply the patch instead of "patch" so that we can
1798 # pick up file adds.
1799 # The --index flag means: also insert into the index (so we catch adds).
1800 cmd = ['git', 'apply', '--index', '-p0']
1801 if directory:
1802 cmd.extend(('--directory', directory))
1803 if reject:
1804 cmd.append('--reject')
1805 elif IsGitVersionAtLeast('1.7.12'):
1806 cmd.append('--3way')
1807 try:
1808 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
1809 stdin=patch_data, stdout=subprocess2.VOID)
1810 except subprocess2.CalledProcessError:
1811 print 'Failed to apply the patch'
1812 return 1
1813
1814 # If we had an issue, commit the current state and register the issue.
1815 if not nocommit:
1816 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
1817 'patch from issue %(i)s at patchset '
1818 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
1819 % {'i': self.GetIssue(), 'p': patchset})])
1820 self.SetIssue(self.GetIssue())
1821 self.SetPatchset(patchset)
1822 print "Committed patch locally."
1823 else:
1824 print "Patch applied to index."
1825 return 0
1826
1827 @staticmethod
1828 def ParseIssueURL(parsed_url):
1829 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1830 return None
1831 # Typical url: https://domain/<issue_number>[/[other]]
1832 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
1833 if match:
1834 return _RietveldParsedIssueNumberArgument(
1835 issue=int(match.group(1)),
1836 hostname=parsed_url.netloc)
1837 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
1838 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
1839 if match:
1840 return _RietveldParsedIssueNumberArgument(
1841 issue=int(match.group(1)),
1842 patchset=int(match.group(2)),
1843 hostname=parsed_url.netloc,
1844 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
1845 return None
1846
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001847 def CMDUploadChange(self, options, args, change):
1848 """Upload the patch to Rietveld."""
1849 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1850 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001851 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
1852 if options.emulate_svn_auto_props:
1853 upload_args.append('--emulate_svn_auto_props')
1854
1855 change_desc = None
1856
1857 if options.email is not None:
1858 upload_args.extend(['--email', options.email])
1859
1860 if self.GetIssue():
1861 if options.title:
1862 upload_args.extend(['--title', options.title])
1863 if options.message:
1864 upload_args.extend(['--message', options.message])
1865 upload_args.extend(['--issue', str(self.GetIssue())])
1866 print ('This branch is associated with issue %s. '
1867 'Adding patch to that issue.' % self.GetIssue())
1868 else:
1869 if options.title:
1870 upload_args.extend(['--title', options.title])
1871 message = (options.title or options.message or
1872 CreateDescriptionFromLog(args))
1873 change_desc = ChangeDescription(message)
1874 if options.reviewers or options.tbr_owners:
1875 change_desc.update_reviewers(options.reviewers,
1876 options.tbr_owners,
1877 change)
1878 if not options.force:
1879 change_desc.prompt()
1880
1881 if not change_desc.description:
1882 print "Description is empty; aborting."
1883 return 1
1884
1885 upload_args.extend(['--message', change_desc.description])
1886 if change_desc.get_reviewers():
1887 upload_args.append('--reviewers=%s' % ','.join(
1888 change_desc.get_reviewers()))
1889 if options.send_mail:
1890 if not change_desc.get_reviewers():
1891 DieWithError("Must specify reviewers to send email.")
1892 upload_args.append('--send_mail')
1893
1894 # We check this before applying rietveld.private assuming that in
1895 # rietveld.cc only addresses which we can send private CLs to are listed
1896 # if rietveld.private is set, and so we should ignore rietveld.cc only
1897 # when --private is specified explicitly on the command line.
1898 if options.private:
1899 logging.warn('rietveld.cc is ignored since private flag is specified. '
1900 'You need to review and add them manually if necessary.')
1901 cc = self.GetCCListWithoutDefault()
1902 else:
1903 cc = self.GetCCList()
1904 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
1905 if cc:
1906 upload_args.extend(['--cc', cc])
1907
1908 if options.private or settings.GetDefaultPrivateFlag() == "True":
1909 upload_args.append('--private')
1910
1911 upload_args.extend(['--git_similarity', str(options.similarity)])
1912 if not options.find_copies:
1913 upload_args.extend(['--git_no_find_copies'])
1914
1915 # Include the upstream repo's URL in the change -- this is useful for
1916 # projects that have their source spread across multiple repos.
1917 remote_url = self.GetGitBaseUrlFromConfig()
1918 if not remote_url:
1919 if settings.GetIsGitSvn():
1920 remote_url = self.GetGitSvnRemoteUrl()
1921 else:
1922 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
1923 remote_url = '%s@%s' % (self.GetRemoteUrl(),
1924 self.GetUpstreamBranch().split('/')[-1])
1925 if remote_url:
1926 upload_args.extend(['--base_url', remote_url])
1927 remote, remote_branch = self.GetRemoteBranch()
1928 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
1929 settings.GetPendingRefPrefix())
1930 if target_ref:
1931 upload_args.extend(['--target_ref', target_ref])
1932
1933 # Look for dependent patchsets. See crbug.com/480453 for more details.
1934 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
1935 upstream_branch = ShortBranchName(upstream_branch)
1936 if remote is '.':
1937 # A local branch is being tracked.
1938 local_branch = ShortBranchName(upstream_branch)
1939 if settings.GetIsSkipDependencyUpload(local_branch):
1940 print
1941 print ('Skipping dependency patchset upload because git config '
1942 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
1943 print
1944 else:
1945 auth_config = auth.extract_auth_config_from_options(options)
1946 branch_cl = Changelist(branchref=local_branch,
1947 auth_config=auth_config)
1948 branch_cl_issue_url = branch_cl.GetIssueURL()
1949 branch_cl_issue = branch_cl.GetIssue()
1950 branch_cl_patchset = branch_cl.GetPatchset()
1951 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
1952 upload_args.extend(
1953 ['--depends_on_patchset', '%s:%s' % (
1954 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001955 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001956 '\n'
1957 'The current branch (%s) is tracking a local branch (%s) with '
1958 'an associated CL.\n'
1959 'Adding %s/#ps%s as a dependency patchset.\n'
1960 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
1961 branch_cl_patchset))
1962
1963 project = settings.GetProject()
1964 if project:
1965 upload_args.extend(['--project', project])
1966
1967 if options.cq_dry_run:
1968 upload_args.extend(['--cq_dry_run'])
1969
1970 try:
1971 upload_args = ['upload'] + upload_args + args
1972 logging.info('upload.RealMain(%s)', upload_args)
1973 issue, patchset = upload.RealMain(upload_args)
1974 issue = int(issue)
1975 patchset = int(patchset)
1976 except KeyboardInterrupt:
1977 sys.exit(1)
1978 except:
1979 # If we got an exception after the user typed a description for their
1980 # change, back up the description before re-raising.
1981 if change_desc:
1982 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1983 print('\nGot exception while uploading -- saving description to %s\n' %
1984 backup_path)
1985 backup_file = open(backup_path, 'w')
1986 backup_file.write(change_desc.description)
1987 backup_file.close()
1988 raise
1989
1990 if not self.GetIssue():
1991 self.SetIssue(issue)
1992 self.SetPatchset(patchset)
1993
1994 if options.use_commit_queue:
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001995 self.SetCQState(_CQState.COMMIT)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001996 return 0
1997
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001998
1999class _GerritChangelistImpl(_ChangelistCodereviewBase):
2000 def __init__(self, changelist, auth_config=None):
2001 # auth_config is Rietveld thing, kept here to preserve interface only.
2002 super(_GerritChangelistImpl, self).__init__(changelist)
2003 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002004 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002005 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002006 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002007
2008 def _GetGerritHost(self):
2009 # Lazy load of configs.
2010 self.GetCodereviewServer()
2011 return self._gerrit_host
2012
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002013 def _GetGitHost(self):
2014 """Returns git host to be used when uploading change to Gerrit."""
2015 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2016
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002017 def GetCodereviewServer(self):
2018 if not self._gerrit_server:
2019 # If we're on a branch then get the server potentially associated
2020 # with that branch.
2021 if self.GetIssue():
2022 gerrit_server_setting = self.GetCodereviewServerSetting()
2023 if gerrit_server_setting:
2024 self._gerrit_server = RunGit(['config', gerrit_server_setting],
2025 error_ok=True).strip()
2026 if self._gerrit_server:
2027 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
2028 if not self._gerrit_server:
2029 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2030 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002031 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002032 parts[0] = parts[0] + '-review'
2033 self._gerrit_host = '.'.join(parts)
2034 self._gerrit_server = 'https://%s' % self._gerrit_host
2035 return self._gerrit_server
2036
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002037 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00002038 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002039 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002040
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002041 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002042 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002043 if settings.GetGerritSkipEnsureAuthenticated():
2044 # For projects with unusual authentication schemes.
2045 # See http://crbug.com/603378.
2046 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002047 # Lazy-loader to identify Gerrit and Git hosts.
2048 if gerrit_util.GceAuthenticator.is_gce():
2049 return
2050 self.GetCodereviewServer()
2051 git_host = self._GetGitHost()
2052 assert self._gerrit_server and self._gerrit_host
2053 cookie_auth = gerrit_util.CookiesAuthenticator()
2054
2055 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2056 git_auth = cookie_auth.get_auth_header(git_host)
2057 if gerrit_auth and git_auth:
2058 if gerrit_auth == git_auth:
2059 return
2060 print((
2061 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2062 ' Check your %s or %s file for credentials of hosts:\n'
2063 ' %s\n'
2064 ' %s\n'
2065 ' %s') %
2066 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2067 git_host, self._gerrit_host,
2068 cookie_auth.get_new_password_message(git_host)))
2069 if not force:
2070 ask_for_data('If you know what you are doing, press Enter to continue, '
2071 'Ctrl+C to abort.')
2072 return
2073 else:
2074 missing = (
2075 [] if gerrit_auth else [self._gerrit_host] +
2076 [] if git_auth else [git_host])
2077 DieWithError('Credentials for the following hosts are required:\n'
2078 ' %s\n'
2079 'These are read from %s (or legacy %s)\n'
2080 '%s' % (
2081 '\n '.join(missing),
2082 cookie_auth.get_gitcookies_path(),
2083 cookie_auth.get_netrc_path(),
2084 cookie_auth.get_new_password_message(git_host)))
2085
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002086
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002087 def PatchsetSetting(self):
2088 """Return the git setting that stores this change's most recent patchset."""
2089 return 'branch.%s.gerritpatchset' % self.GetBranch()
2090
2091 def GetCodereviewServerSetting(self):
2092 """Returns the git setting that stores this change's Gerrit server."""
2093 branch = self.GetBranch()
2094 if branch:
2095 return 'branch.%s.gerritserver' % branch
2096 return None
2097
2098 def GetRieveldObjForPresubmit(self):
2099 class ThisIsNotRietveldIssue(object):
2100 def __nonzero__(self):
2101 # This is a hack to make presubmit_support think that rietveld is not
2102 # defined, yet still ensure that calls directly result in a decent
2103 # exception message below.
2104 return False
2105
2106 def __getattr__(self, attr):
2107 print(
2108 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2109 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2110 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2111 'or use Rietveld for codereview.\n'
2112 'See also http://crbug.com/579160.' % attr)
2113 raise NotImplementedError()
2114 return ThisIsNotRietveldIssue()
2115
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002116 def GetGerritObjForPresubmit(self):
2117 return presubmit_support.GerritAccessor(self._GetGerritHost())
2118
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002119 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002120 """Apply a rough heuristic to give a simple summary of an issue's review
2121 or CQ status, assuming adherence to a common workflow.
2122
2123 Returns None if no issue for this branch, or one of the following keywords:
2124 * 'error' - error from review tool (including deleted issues)
2125 * 'unsent' - no reviewers added
2126 * 'waiting' - waiting for review
2127 * 'reply' - waiting for owner to reply to review
2128 * 'not lgtm' - Code-Review -2 from at least one approved reviewer
2129 * 'lgtm' - Code-Review +2 from at least one approved reviewer
2130 * 'commit' - in the commit queue
2131 * 'closed' - abandoned
2132 """
2133 if not self.GetIssue():
2134 return None
2135
2136 try:
2137 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
2138 except httplib.HTTPException:
2139 return 'error'
2140
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002141 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002142 return 'closed'
2143
2144 cq_label = data['labels'].get('Commit-Queue', {})
2145 if cq_label:
2146 # Vote value is a stringified integer, which we expect from 0 to 2.
2147 vote_value = cq_label.get('value', '0')
2148 vote_text = cq_label.get('values', {}).get(vote_value, '')
2149 if vote_text.lower() == 'commit':
2150 return 'commit'
2151
2152 lgtm_label = data['labels'].get('Code-Review', {})
2153 if lgtm_label:
2154 if 'rejected' in lgtm_label:
2155 return 'not lgtm'
2156 if 'approved' in lgtm_label:
2157 return 'lgtm'
2158
2159 if not data.get('reviewers', {}).get('REVIEWER', []):
2160 return 'unsent'
2161
2162 messages = data.get('messages', [])
2163 if messages:
2164 owner = data['owner'].get('_account_id')
2165 last_message_author = messages[-1].get('author', {}).get('_account_id')
2166 if owner != last_message_author:
2167 # Some reply from non-owner.
2168 return 'reply'
2169
2170 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002171
2172 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002173 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002174 return data['revisions'][data['current_revision']]['_number']
2175
2176 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002177 data = self._GetChangeDetail(['CURRENT_REVISION'])
2178 current_rev = data['current_revision']
2179 url = data['revisions'][current_rev]['fetch']['http']['url']
2180 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002181
2182 def UpdateDescriptionRemote(self, description):
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002183 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
2184 description)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002185
2186 def CloseIssue(self):
2187 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2188
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002189 def GetApprovingReviewers(self):
2190 """Returns a list of reviewers approving the change.
2191
2192 Note: not necessarily committers.
2193 """
2194 raise NotImplementedError()
2195
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002196 def SubmitIssue(self, wait_for_merge=True):
2197 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2198 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002199
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002200 def _GetChangeDetail(self, options=None, issue=None):
2201 options = options or []
2202 issue = issue or self.GetIssue()
2203 assert issue, 'issue required to query Gerrit'
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002204 return gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2205 options)
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002206
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002207 def CMDLand(self, force, bypass_hooks, verbose):
2208 if git_common.is_dirty_git_tree('land'):
2209 return 1
2210 differs = True
2211 last_upload = RunGit(['config',
2212 'branch.%s.gerritsquashhash' % self.GetBranch()],
2213 error_ok=True).strip()
2214 # Note: git diff outputs nothing if there is no diff.
2215 if not last_upload or RunGit(['diff', last_upload]).strip():
2216 print('WARNING: some changes from local branch haven\'t been uploaded')
2217 else:
2218 detail = self._GetChangeDetail(['CURRENT_REVISION'])
2219 if detail['current_revision'] == last_upload:
2220 differs = False
2221 else:
2222 print('WARNING: local branch contents differ from latest uploaded '
2223 'patchset')
2224 if differs:
2225 if not force:
2226 ask_for_data(
2227 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2228 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2229 elif not bypass_hooks:
2230 hook_results = self.RunHook(
2231 committing=True,
2232 may_prompt=not force,
2233 verbose=verbose,
2234 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2235 if not hook_results.should_continue():
2236 return 1
2237
2238 self.SubmitIssue(wait_for_merge=True)
2239 print('Issue %s has been submitted.' % self.GetIssueURL())
2240 return 0
2241
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002242 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2243 directory):
2244 assert not reject
2245 assert not nocommit
2246 assert not directory
2247 assert parsed_issue_arg.valid
2248
2249 self._changelist.issue = parsed_issue_arg.issue
2250
2251 if parsed_issue_arg.hostname:
2252 self._gerrit_host = parsed_issue_arg.hostname
2253 self._gerrit_server = 'https://%s' % self._gerrit_host
2254
2255 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2256
2257 if not parsed_issue_arg.patchset:
2258 # Use current revision by default.
2259 revision_info = detail['revisions'][detail['current_revision']]
2260 patchset = int(revision_info['_number'])
2261 else:
2262 patchset = parsed_issue_arg.patchset
2263 for revision_info in detail['revisions'].itervalues():
2264 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2265 break
2266 else:
2267 DieWithError('Couldn\'t find patchset %i in issue %i' %
2268 (parsed_issue_arg.patchset, self.GetIssue()))
2269
2270 fetch_info = revision_info['fetch']['http']
2271 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2272 RunGit(['cherry-pick', 'FETCH_HEAD'])
2273 self.SetIssue(self.GetIssue())
2274 self.SetPatchset(patchset)
2275 print('Committed patch for issue %i pathset %i locally' %
2276 (self.GetIssue(), self.GetPatchset()))
2277 return 0
2278
2279 @staticmethod
2280 def ParseIssueURL(parsed_url):
2281 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2282 return None
2283 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2284 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2285 # Short urls like https://domain/<issue_number> can be used, but don't allow
2286 # specifying the patchset (you'd 404), but we allow that here.
2287 if parsed_url.path == '/':
2288 part = parsed_url.fragment
2289 else:
2290 part = parsed_url.path
2291 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2292 if match:
2293 return _ParsedIssueNumberArgument(
2294 issue=int(match.group(2)),
2295 patchset=int(match.group(4)) if match.group(4) else None,
2296 hostname=parsed_url.netloc)
2297 return None
2298
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002299 def CMDUploadChange(self, options, args, change):
2300 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002301 if options.squash and options.no_squash:
2302 DieWithError('Can only use one of --squash or --no-squash')
2303 options.squash = ((settings.GetSquashGerritUploads() or options.squash) and
2304 not options.no_squash)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002305 # We assume the remote called "origin" is the one we want.
2306 # It is probably not worthwhile to support different workflows.
2307 gerrit_remote = 'origin'
2308
2309 remote, remote_branch = self.GetRemoteBranch()
2310 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2311 pending_prefix='')
2312
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002313 if options.squash:
2314 if not self.GetIssue():
2315 # TODO(tandrii): deperecate this after 2016Q2. Backwards compatibility
2316 # with shadow branch, which used to contain change-id for a given
2317 # branch, using which we can fetch actual issue number and set it as the
2318 # property of the branch, which is the new way.
2319 message = RunGitSilent([
2320 'show', '--format=%B', '-s',
2321 'refs/heads/git_cl_uploads/%s' % self.GetBranch()])
2322 if message:
2323 change_ids = git_footers.get_footer_change_id(message.strip())
2324 if change_ids and len(change_ids) == 1:
2325 details = self._GetChangeDetail(issue=change_ids[0])
2326 if details:
2327 print('WARNING: found old upload in branch git_cl_uploads/%s '
2328 'corresponding to issue %s' %
2329 (self.GetBranch(), details['_number']))
2330 self.SetIssue(details['_number'])
2331 if not self.GetIssue():
2332 DieWithError(
2333 '\n' # For readability of the blob below.
2334 'Found old upload in branch git_cl_uploads/%s, '
2335 'but failed to find corresponding Gerrit issue.\n'
2336 'If you know the issue number, set it manually first:\n'
2337 ' git cl issue 123456\n'
2338 'If you intended to upload this CL as new issue, '
2339 'just delete or rename the old upload branch:\n'
2340 ' git rename-branch git_cl_uploads/%s old_upload-%s\n'
2341 'After that, please run git cl upload again.' %
2342 tuple([self.GetBranch()] * 3))
2343 # End of backwards compatability.
2344
2345 if self.GetIssue():
2346 # Try to get the message from a previous upload.
2347 message = self.GetDescription()
2348 if not message:
2349 DieWithError(
2350 'failed to fetch description from current Gerrit issue %d\n'
2351 '%s' % (self.GetIssue(), self.GetIssueURL()))
2352 change_id = self._GetChangeDetail()['change_id']
2353 while True:
2354 footer_change_ids = git_footers.get_footer_change_id(message)
2355 if footer_change_ids == [change_id]:
2356 break
2357 if not footer_change_ids:
2358 message = git_footers.add_footer_change_id(message, change_id)
2359 print('WARNING: appended missing Change-Id to issue description')
2360 continue
2361 # There is already a valid footer but with different or several ids.
2362 # Doing this automatically is non-trivial as we don't want to lose
2363 # existing other footers, yet we want to append just 1 desired
2364 # Change-Id. Thus, just create a new footer, but let user verify the
2365 # new description.
2366 message = '%s\n\nChange-Id: %s' % (message, change_id)
2367 print(
2368 'WARNING: issue %s has Change-Id footer(s):\n'
2369 ' %s\n'
2370 'but issue has Change-Id %s, according to Gerrit.\n'
2371 'Please, check the proposed correction to the description, '
2372 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2373 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2374 change_id))
2375 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2376 if not options.force:
2377 change_desc = ChangeDescription(message)
2378 change_desc.prompt()
2379 message = change_desc.description
2380 if not message:
2381 DieWithError("Description is empty. Aborting...")
2382 # Continue the while loop.
2383 # Sanity check of this code - we should end up with proper message
2384 # footer.
2385 assert [change_id] == git_footers.get_footer_change_id(message)
2386 change_desc = ChangeDescription(message)
2387 else:
2388 change_desc = ChangeDescription(
2389 options.message or CreateDescriptionFromLog(args))
2390 if not options.force:
2391 change_desc.prompt()
2392 if not change_desc.description:
2393 DieWithError("Description is empty. Aborting...")
2394 message = change_desc.description
2395 change_ids = git_footers.get_footer_change_id(message)
2396 if len(change_ids) > 1:
2397 DieWithError('too many Change-Id footers, at most 1 allowed.')
2398 if not change_ids:
2399 # Generate the Change-Id automatically.
2400 message = git_footers.add_footer_change_id(
2401 message, GenerateGerritChangeId(message))
2402 change_desc.set_description(message)
2403 change_ids = git_footers.get_footer_change_id(message)
2404 assert len(change_ids) == 1
2405 change_id = change_ids[0]
2406
2407 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2408 if remote is '.':
2409 # If our upstream branch is local, we base our squashed commit on its
2410 # squashed version.
2411 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2412 # Check the squashed hash of the parent.
2413 parent = RunGit(['config',
2414 'branch.%s.gerritsquashhash' % upstream_branch_name],
2415 error_ok=True).strip()
2416 # Verify that the upstream branch has been uploaded too, otherwise
2417 # Gerrit will create additional CLs when uploading.
2418 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2419 RunGitSilent(['rev-parse', parent + ':'])):
2420 # TODO(tandrii): remove "old depot_tools" part on April 12, 2016.
2421 DieWithError(
2422 'Upload upstream branch %s first.\n'
2423 'Note: maybe you\'ve uploaded it with --no-squash or with an old '
2424 'version of depot_tools. If so, then re-upload it with:\n'
2425 ' git cl upload --squash\n' % upstream_branch_name)
2426 else:
2427 parent = self.GetCommonAncestorWithUpstream()
2428
2429 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2430 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2431 '-m', message]).strip()
2432 else:
2433 change_desc = ChangeDescription(
2434 options.message or CreateDescriptionFromLog(args))
2435 if not change_desc.description:
2436 DieWithError("Description is empty. Aborting...")
2437
2438 if not git_footers.get_footer_change_id(change_desc.description):
2439 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002440 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2441 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002442 ref_to_push = 'HEAD'
2443 parent = '%s/%s' % (gerrit_remote, branch)
2444 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2445
2446 assert change_desc
2447 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2448 ref_to_push)]).splitlines()
2449 if len(commits) > 1:
2450 print('WARNING: This will upload %d commits. Run the following command '
2451 'to see which commits will be uploaded: ' % len(commits))
2452 print('git log %s..%s' % (parent, ref_to_push))
2453 print('You can also use `git squash-branch` to squash these into a '
2454 'single commit.')
2455 ask_for_data('About to upload; enter to confirm.')
2456
2457 if options.reviewers or options.tbr_owners:
2458 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2459 change)
2460
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002461 # Extra options that can be specified at push time. Doc:
2462 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2463 refspec_opts = []
2464 if options.title:
2465 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2466 # reverse on its side.
2467 if '_' in options.title:
2468 print('WARNING: underscores in title will be converted to spaces.')
2469 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2470
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002471 if options.send_mail:
2472 if not change_desc.get_reviewers():
2473 DieWithError('Must specify reviewers to send email.')
2474 refspec_opts.append('notify=ALL')
2475 else:
2476 refspec_opts.append('notify=NONE')
2477
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002478 cc = self.GetCCList().split(',')
2479 if options.cc:
2480 cc.extend(options.cc)
2481 cc = filter(None, cc)
2482 if cc:
tandrii@chromium.org0b2d7072016-04-18 16:19:03 +00002483 # refspec_opts.extend('cc=' + email.strip() for email in cc)
2484 # TODO(tandrii): enable this back. http://crbug.com/604377
2485 print('WARNING: Gerrit doesn\'t yet support cc-ing arbitrary emails.\n'
2486 ' Ignoring cc-ed emails. See http://crbug.com/604377.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002487
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002488 if change_desc.get_reviewers():
2489 refspec_opts.extend('r=' + email.strip()
2490 for email in change_desc.get_reviewers())
2491
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002492 refspec_suffix = ''
2493 if refspec_opts:
2494 refspec_suffix = '%' + ','.join(refspec_opts)
2495 assert ' ' not in refspec_suffix, (
2496 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002497 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002498
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002499 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002500 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002501 print_stdout=True,
2502 # Flush after every line: useful for seeing progress when running as
2503 # recipe.
2504 filter_fn=lambda _: sys.stdout.flush())
2505
2506 if options.squash:
2507 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2508 change_numbers = [m.group(1)
2509 for m in map(regex.match, push_stdout.splitlines())
2510 if m]
2511 if len(change_numbers) != 1:
2512 DieWithError(
2513 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2514 'Change-Id: %s') % (len(change_numbers), change_id))
2515 self.SetIssue(change_numbers[0])
2516 RunGit(['config', 'branch.%s.gerritsquashhash' % self.GetBranch(),
2517 ref_to_push])
2518 return 0
2519
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002520 def _AddChangeIdToCommitMessage(self, options, args):
2521 """Re-commits using the current message, assumes the commit hook is in
2522 place.
2523 """
2524 log_desc = options.message or CreateDescriptionFromLog(args)
2525 git_command = ['commit', '--amend', '-m', log_desc]
2526 RunGit(git_command)
2527 new_log_desc = CreateDescriptionFromLog(args)
2528 if git_footers.get_footer_change_id(new_log_desc):
2529 print 'git-cl: Added Change-Id to commit message.'
2530 return new_log_desc
2531 else:
2532 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002533
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002534 def SetCQState(self, new_state):
2535 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
2536 # TODO(tandrii): maybe allow configurability in codereview.settings or by
2537 # self-discovery of label config for this CL using REST API.
2538 vote_map = {
2539 _CQState.NONE: 0,
2540 _CQState.DRY_RUN: 1,
2541 _CQState.COMMIT : 2,
2542 }
2543 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2544 labels={'Commit-Queue': vote_map[new_state]})
2545
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002546
2547_CODEREVIEW_IMPLEMENTATIONS = {
2548 'rietveld': _RietveldChangelistImpl,
2549 'gerrit': _GerritChangelistImpl,
2550}
2551
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002552
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002553def _add_codereview_select_options(parser):
2554 """Appends --gerrit and --rietveld options to force specific codereview."""
2555 parser.codereview_group = optparse.OptionGroup(
2556 parser, 'EXPERIMENTAL! Codereview override options')
2557 parser.add_option_group(parser.codereview_group)
2558 parser.codereview_group.add_option(
2559 '--gerrit', action='store_true',
2560 help='Force the use of Gerrit for codereview')
2561 parser.codereview_group.add_option(
2562 '--rietveld', action='store_true',
2563 help='Force the use of Rietveld for codereview')
2564
2565
2566def _process_codereview_select_options(parser, options):
2567 if options.gerrit and options.rietveld:
2568 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2569 options.forced_codereview = None
2570 if options.gerrit:
2571 options.forced_codereview = 'gerrit'
2572 elif options.rietveld:
2573 options.forced_codereview = 'rietveld'
2574
2575
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002576class ChangeDescription(object):
2577 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002578 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002579 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002580
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002581 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002582 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002583
agable@chromium.org42c20792013-09-12 17:34:49 +00002584 @property # www.logilab.org/ticket/89786
2585 def description(self): # pylint: disable=E0202
2586 return '\n'.join(self._description_lines)
2587
2588 def set_description(self, desc):
2589 if isinstance(desc, basestring):
2590 lines = desc.splitlines()
2591 else:
2592 lines = [line.rstrip() for line in desc]
2593 while lines and not lines[0]:
2594 lines.pop(0)
2595 while lines and not lines[-1]:
2596 lines.pop(-1)
2597 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002598
piman@chromium.org336f9122014-09-04 02:16:55 +00002599 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002600 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002601 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002602 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002603 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002604 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002605
agable@chromium.org42c20792013-09-12 17:34:49 +00002606 # Get the set of R= and TBR= lines and remove them from the desciption.
2607 regexp = re.compile(self.R_LINE)
2608 matches = [regexp.match(line) for line in self._description_lines]
2609 new_desc = [l for i, l in enumerate(self._description_lines)
2610 if not matches[i]]
2611 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002612
agable@chromium.org42c20792013-09-12 17:34:49 +00002613 # Construct new unified R= and TBR= lines.
2614 r_names = []
2615 tbr_names = []
2616 for match in matches:
2617 if not match:
2618 continue
2619 people = cleanup_list([match.group(2).strip()])
2620 if match.group(1) == 'TBR':
2621 tbr_names.extend(people)
2622 else:
2623 r_names.extend(people)
2624 for name in r_names:
2625 if name not in reviewers:
2626 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002627 if add_owners_tbr:
2628 owners_db = owners.Database(change.RepositoryRoot(),
2629 fopen=file, os_path=os.path, glob=glob.glob)
2630 all_reviewers = set(tbr_names + reviewers)
2631 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2632 all_reviewers)
2633 tbr_names.extend(owners_db.reviewers_for(missing_files,
2634 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002635 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2636 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2637
2638 # Put the new lines in the description where the old first R= line was.
2639 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2640 if 0 <= line_loc < len(self._description_lines):
2641 if new_tbr_line:
2642 self._description_lines.insert(line_loc, new_tbr_line)
2643 if new_r_line:
2644 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002645 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002646 if new_r_line:
2647 self.append_footer(new_r_line)
2648 if new_tbr_line:
2649 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002650
2651 def prompt(self):
2652 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002653 self.set_description([
2654 '# Enter a description of the change.',
2655 '# This will be displayed on the codereview site.',
2656 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002657 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002658 '--------------------',
2659 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002660
agable@chromium.org42c20792013-09-12 17:34:49 +00002661 regexp = re.compile(self.BUG_LINE)
2662 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00002663 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00002664 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002665 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002666 if not content:
2667 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002668 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002669
2670 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002671 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2672 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002673 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002674 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002675
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002676 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00002677 if self._description_lines:
2678 # Add an empty line if either the last line or the new line isn't a tag.
2679 last_line = self._description_lines[-1]
2680 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
2681 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2682 self._description_lines.append('')
2683 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002684
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002685 def get_reviewers(self):
2686 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002687 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
2688 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002689 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002690
2691
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002692def get_approving_reviewers(props):
2693 """Retrieves the reviewers that approved a CL from the issue properties with
2694 messages.
2695
2696 Note that the list may contain reviewers that are not committer, thus are not
2697 considered by the CQ.
2698 """
2699 return sorted(
2700 set(
2701 message['sender']
2702 for message in props['messages']
2703 if message['approval'] and message['sender'] in props['reviewers']
2704 )
2705 )
2706
2707
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002708def FindCodereviewSettingsFile(filename='codereview.settings'):
2709 """Finds the given file starting in the cwd and going up.
2710
2711 Only looks up to the top of the repository unless an
2712 'inherit-review-settings-ok' file exists in the root of the repository.
2713 """
2714 inherit_ok_file = 'inherit-review-settings-ok'
2715 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002716 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002717 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2718 root = '/'
2719 while True:
2720 if filename in os.listdir(cwd):
2721 if os.path.isfile(os.path.join(cwd, filename)):
2722 return open(os.path.join(cwd, filename))
2723 if cwd == root:
2724 break
2725 cwd = os.path.dirname(cwd)
2726
2727
2728def LoadCodereviewSettingsFromFile(fileobj):
2729 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002730 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002731
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002732 def SetProperty(name, setting, unset_error_ok=False):
2733 fullname = 'rietveld.' + name
2734 if setting in keyvals:
2735 RunGit(['config', fullname, keyvals[setting]])
2736 else:
2737 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2738
2739 SetProperty('server', 'CODE_REVIEW_SERVER')
2740 # Only server setting is required. Other settings can be absent.
2741 # In that case, we ignore errors raised during option deletion attempt.
2742 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002743 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002744 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2745 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002746 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002747 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002748 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2749 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002750 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002751 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002752 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002753 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2754 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002755
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002756 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002757 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002758
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002759 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
2760 RunGit(['config', 'gerrit.squash-uploads',
2761 keyvals['GERRIT_SQUASH_UPLOADS']])
2762
tandrii@chromium.org28253532016-04-14 13:46:56 +00002763 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00002764 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00002765 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
2766
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002767 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2768 #should be of the form
2769 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2770 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2771 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2772 keyvals['ORIGIN_URL_CONFIG']])
2773
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002774
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002775def urlretrieve(source, destination):
2776 """urllib is broken for SSL connections via a proxy therefore we
2777 can't use urllib.urlretrieve()."""
2778 with open(destination, 'w') as f:
2779 f.write(urllib2.urlopen(source).read())
2780
2781
ukai@chromium.org712d6102013-11-27 00:52:58 +00002782def hasSheBang(fname):
2783 """Checks fname is a #! script."""
2784 with open(fname) as f:
2785 return f.read(2).startswith('#!')
2786
2787
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00002788# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
2789def DownloadHooks(*args, **kwargs):
2790 pass
2791
2792
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002793def DownloadGerritHook(force):
2794 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002795
2796 Args:
2797 force: True to update hooks. False to install hooks if not present.
2798 """
2799 if not settings.GetIsGerrit():
2800 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00002801 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002802 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2803 if not os.access(dst, os.X_OK):
2804 if os.path.exists(dst):
2805 if not force:
2806 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002807 try:
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002808 print(
2809 'WARNING: installing Gerrit commit-msg hook.\n'
2810 ' This behavior of git cl will soon be disabled.\n'
2811 ' See bug http://crbug.com/579176.')
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002812 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002813 if not hasSheBang(dst):
2814 DieWithError('Not a script: %s\n'
2815 'You need to download from\n%s\n'
2816 'into .git/hooks/commit-msg and '
2817 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002818 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2819 except Exception:
2820 if os.path.exists(dst):
2821 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002822 DieWithError('\nFailed to download hooks.\n'
2823 'You need to download from\n%s\n'
2824 'into .git/hooks/commit-msg and '
2825 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002826
2827
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002828
2829def GetRietveldCodereviewSettingsInteractively():
2830 """Prompt the user for settings."""
2831 server = settings.GetDefaultServerUrl(error_ok=True)
2832 prompt = 'Rietveld server (host[:port])'
2833 prompt += ' [%s]' % (server or DEFAULT_SERVER)
2834 newserver = ask_for_data(prompt + ':')
2835 if not server and not newserver:
2836 newserver = DEFAULT_SERVER
2837 if newserver:
2838 newserver = gclient_utils.UpgradeToHttps(newserver)
2839 if newserver != server:
2840 RunGit(['config', 'rietveld.server', newserver])
2841
2842 def SetProperty(initial, caption, name, is_url):
2843 prompt = caption
2844 if initial:
2845 prompt += ' ("x" to clear) [%s]' % initial
2846 new_val = ask_for_data(prompt + ':')
2847 if new_val == 'x':
2848 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
2849 elif new_val:
2850 if is_url:
2851 new_val = gclient_utils.UpgradeToHttps(new_val)
2852 if new_val != initial:
2853 RunGit(['config', 'rietveld.' + name, new_val])
2854
2855 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
2856 SetProperty(settings.GetDefaultPrivateFlag(),
2857 'Private flag (rietveld only)', 'private', False)
2858 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
2859 'tree-status-url', False)
2860 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
2861 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
2862 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
2863 'run-post-upload-hook', False)
2864
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002865@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002866def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002867 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002868
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002869 print('WARNING: git cl config works for Rietveld only.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002870 'For Gerrit, see http://crbug.com/603116.')
2871 # TODO(tandrii): add Gerrit support as part of http://crbug.com/603116.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00002872 parser.add_option('--activate-update', action='store_true',
2873 help='activate auto-updating [rietveld] section in '
2874 '.git/config')
2875 parser.add_option('--deactivate-update', action='store_true',
2876 help='deactivate auto-updating [rietveld] section in '
2877 '.git/config')
2878 options, args = parser.parse_args(args)
2879
2880 if options.deactivate_update:
2881 RunGit(['config', 'rietveld.autoupdate', 'false'])
2882 return
2883
2884 if options.activate_update:
2885 RunGit(['config', '--unset', 'rietveld.autoupdate'])
2886 return
2887
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002888 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002889 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002890 return 0
2891
2892 url = args[0]
2893 if not url.endswith('codereview.settings'):
2894 url = os.path.join(url, 'codereview.settings')
2895
2896 # Load code review settings and download hooks (if available).
2897 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
2898 return 0
2899
2900
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002901def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002902 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002903 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
2904 branch = ShortBranchName(branchref)
2905 _, args = parser.parse_args(args)
2906 if not args:
2907 print("Current base-url:")
2908 return RunGit(['config', 'branch.%s.base-url' % branch],
2909 error_ok=False).strip()
2910 else:
2911 print("Setting base-url to %s" % args[0])
2912 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
2913 error_ok=False).strip()
2914
2915
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002916def color_for_status(status):
2917 """Maps a Changelist status to color, for CMDstatus and other tools."""
2918 return {
2919 'unsent': Fore.RED,
2920 'waiting': Fore.BLUE,
2921 'reply': Fore.YELLOW,
2922 'lgtm': Fore.GREEN,
2923 'commit': Fore.MAGENTA,
2924 'closed': Fore.CYAN,
2925 'error': Fore.WHITE,
2926 }.get(status, Fore.WHITE)
2927
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00002928def fetch_cl_status(branch, auth_config=None):
2929 """Fetches information for an issue and returns (branch, issue, status)."""
2930 cl = Changelist(branchref=branch, auth_config=auth_config)
2931 url = cl.GetIssueURL()
2932 status = cl.GetStatus()
2933
2934 if url and (not status or status == 'error'):
2935 # The issue probably doesn't exist anymore.
2936 url += ' (broken)'
2937
2938 return (branch, url, status)
2939
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002940def get_cl_statuses(
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00002941 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgcf197482016-04-29 20:15:53 +00002942 """Returns a blocking iterable of (branch, issue, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002943
2944 If fine_grained is true, this will fetch CL statuses from the server.
2945 Otherwise, simply indicate if there's a matching url for the given branches.
2946
2947 If max_processes is specified, it is used as the maximum number of processes
2948 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
2949 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00002950
2951 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002952 """
calamity@chromium.orgcf197482016-04-29 20:15:53 +00002953 def fetch(branch):
2954 if not branch:
2955 return None
2956
2957 return fetch_cl_status(branch, auth_config=auth_config)
2958
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002959 # Silence upload.py otherwise it becomes unwieldly.
2960 upload.verbosity = 0
2961
2962 if fine_grained:
2963 # Process one branch synchronously to work through authentication, then
2964 # spawn processes to process all the other branches in parallel.
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00002965 if branches:
calamity@chromium.orgcf197482016-04-29 20:15:53 +00002966
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00002967 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002968
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00002969 branches_to_fetch = branches[1:]
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002970 pool = ThreadPool(
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00002971 min(max_processes, len(branches_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002972 if max_processes is not None
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00002973 else len(branches_to_fetch))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00002974
2975 fetched_branches = set()
2976 it = pool.imap_unordered(fetch, branches_to_fetch).__iter__()
2977 while True:
2978 try:
2979 row = it.next(timeout=5)
2980 except multiprocessing.TimeoutError:
2981 break
2982
2983 fetched_branches.add(row[0])
2984 yield row
2985
2986 # Add any branches that failed to fetch.
2987 for b in set(branches_to_fetch) - fetched_branches:
2988 cl = Changelist(branchref=b, auth_config=auth_config)
2989 yield (b, cl.GetIssueURL() if b else None, 'error')
2990
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002991 else:
2992 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00002993 for b in branches:
2994 cl = Changelist(branchref=b, auth_config=auth_config)
calamity@chromium.orgcf197482016-04-29 20:15:53 +00002995 url = cl.GetIssueURL() if b else None
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00002996 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002997
rmistry@google.com2dd99862015-06-22 12:22:18 +00002998
2999def upload_branch_deps(cl, args):
3000 """Uploads CLs of local branches that are dependents of the current branch.
3001
3002 If the local branch dependency tree looks like:
3003 test1 -> test2.1 -> test3.1
3004 -> test3.2
3005 -> test2.2 -> test3.3
3006
3007 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3008 run on the dependent branches in this order:
3009 test2.1, test3.1, test3.2, test2.2, test3.3
3010
3011 Note: This function does not rebase your local dependent branches. Use it when
3012 you make a change to the parent branch that will not conflict with its
3013 dependent branches, and you would like their dependencies updated in
3014 Rietveld.
3015 """
3016 if git_common.is_dirty_git_tree('upload-branch-deps'):
3017 return 1
3018
3019 root_branch = cl.GetBranch()
3020 if root_branch is None:
3021 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3022 'Get on a branch!')
3023 if not cl.GetIssue() or not cl.GetPatchset():
3024 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3025 'patchset dependencies without an uploaded CL.')
3026
3027 branches = RunGit(['for-each-ref',
3028 '--format=%(refname:short) %(upstream:short)',
3029 'refs/heads'])
3030 if not branches:
3031 print('No local branches found.')
3032 return 0
3033
3034 # Create a dictionary of all local branches to the branches that are dependent
3035 # on it.
3036 tracked_to_dependents = collections.defaultdict(list)
3037 for b in branches.splitlines():
3038 tokens = b.split()
3039 if len(tokens) == 2:
3040 branch_name, tracked = tokens
3041 tracked_to_dependents[tracked].append(branch_name)
3042
3043 print
3044 print 'The dependent local branches of %s are:' % root_branch
3045 dependents = []
3046 def traverse_dependents_preorder(branch, padding=''):
3047 dependents_to_process = tracked_to_dependents.get(branch, [])
3048 padding += ' '
3049 for dependent in dependents_to_process:
3050 print '%s%s' % (padding, dependent)
3051 dependents.append(dependent)
3052 traverse_dependents_preorder(dependent, padding)
3053 traverse_dependents_preorder(root_branch)
3054 print
3055
3056 if not dependents:
3057 print 'There are no dependent local branches for %s' % root_branch
3058 return 0
3059
3060 print ('This command will checkout all dependent branches and run '
3061 '"git cl upload".')
3062 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3063
andybons@chromium.org962f9462016-02-03 20:00:42 +00003064 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003065 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003066 args.extend(['-t', 'Updated patchset dependency'])
3067
rmistry@google.com2dd99862015-06-22 12:22:18 +00003068 # Record all dependents that failed to upload.
3069 failures = {}
3070 # Go through all dependents, checkout the branch and upload.
3071 try:
3072 for dependent_branch in dependents:
3073 print
3074 print '--------------------------------------'
3075 print 'Running "git cl upload" from %s:' % dependent_branch
3076 RunGit(['checkout', '-q', dependent_branch])
3077 print
3078 try:
3079 if CMDupload(OptionParser(), args) != 0:
3080 print 'Upload failed for %s!' % dependent_branch
3081 failures[dependent_branch] = 1
3082 except: # pylint: disable=W0702
3083 failures[dependent_branch] = 1
3084 print
3085 finally:
3086 # Swap back to the original root branch.
3087 RunGit(['checkout', '-q', root_branch])
3088
3089 print
3090 print 'Upload complete for dependent branches!'
3091 for dependent_branch in dependents:
3092 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
3093 print ' %s : %s' % (dependent_branch, upload_status)
3094 print
3095
3096 return 0
3097
3098
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003099def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003100 """Show status of changelists.
3101
3102 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003103 - Red not sent for review or broken
3104 - Blue waiting for review
3105 - Yellow waiting for you to reply to review
3106 - Green LGTM'ed
3107 - Magenta in the commit queue
3108 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003109
3110 Also see 'git cl comments'.
3111 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003112 parser.add_option('--field',
3113 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003114 parser.add_option('-f', '--fast', action='store_true',
3115 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003116 parser.add_option(
3117 '-j', '--maxjobs', action='store', type=int,
3118 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003119
3120 auth.add_auth_options(parser)
3121 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003122 if args:
3123 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003124 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003125
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003126 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003127 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003128 if options.field.startswith('desc'):
3129 print cl.GetDescription()
3130 elif options.field == 'id':
3131 issueid = cl.GetIssue()
3132 if issueid:
3133 print issueid
3134 elif options.field == 'patch':
3135 patchset = cl.GetPatchset()
3136 if patchset:
3137 print patchset
3138 elif options.field == 'url':
3139 url = cl.GetIssueURL()
3140 if url:
3141 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003142 return 0
3143
3144 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3145 if not branches:
3146 print('No local branch found.')
3147 return 0
3148
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003149 changes = (
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003150 Changelist(branchref=b, auth_config=auth_config)
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003151 for b in branches.splitlines())
3152 # TODO(tandrii): refactor to use CLs list instead of branches list.
3153 branches = [c.GetBranch() for c in changes]
3154 alignment = max(5, max(len(b) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003155 print 'Branches associated with reviews:'
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003156 output = get_cl_statuses(branches,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003157 fine_grained=not options.fast,
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003158 max_processes=options.maxjobs,
3159 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003160
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003161 branch_statuses = {}
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003162 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
3163 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003164 while branch not in branch_statuses:
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003165 b, i, status = output.next()
3166 branch_statuses[b] = (i, status)
3167 issue_url, status = branch_statuses.pop(branch)
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003168 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003169 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003170 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003171 color = ''
3172 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003173 status_str = '(%s)' % status if status else ''
3174 print ' %*s : %s%s %s%s' % (
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003175 alignment, ShortBranchName(branch), color, issue_url, status_str,
3176 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003177
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003178 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003179 print
3180 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003181 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003182 if not cl.GetIssue():
3183 print 'No issue assigned.'
3184 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003185 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00003186 if not options.fast:
3187 print 'Issue description:'
3188 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003189 return 0
3190
3191
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003192def colorize_CMDstatus_doc():
3193 """To be called once in main() to add colors to git cl status help."""
3194 colors = [i for i in dir(Fore) if i[0].isupper()]
3195
3196 def colorize_line(line):
3197 for color in colors:
3198 if color in line.upper():
3199 # Extract whitespaces first and the leading '-'.
3200 indent = len(line) - len(line.lstrip(' ')) + 1
3201 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3202 return line
3203
3204 lines = CMDstatus.__doc__.splitlines()
3205 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3206
3207
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003208@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003209def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003210 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003211
3212 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003213 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003214 parser.add_option('-r', '--reverse', action='store_true',
3215 help='Lookup the branch(es) for the specified issues. If '
3216 'no issues are specified, all branches with mapped '
3217 'issues will be listed.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003218 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003219 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003220 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003221
dnj@chromium.org406c4402015-03-03 17:22:28 +00003222 if options.reverse:
3223 branches = RunGit(['for-each-ref', 'refs/heads',
3224 '--format=%(refname:short)']).splitlines()
3225
3226 # Reverse issue lookup.
3227 issue_branch_map = {}
3228 for branch in branches:
3229 cl = Changelist(branchref=branch)
3230 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3231 if not args:
3232 args = sorted(issue_branch_map.iterkeys())
3233 for issue in args:
3234 if not issue:
3235 continue
3236 print 'Branch for issue number %s: %s' % (
3237 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
3238 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003239 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003240 if len(args) > 0:
3241 try:
3242 issue = int(args[0])
3243 except ValueError:
3244 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003245 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003246 cl.SetIssue(issue)
3247 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003248 return 0
3249
3250
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003251def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003252 """Shows or posts review comments for any changelist."""
3253 parser.add_option('-a', '--add-comment', dest='comment',
3254 help='comment to add to an issue')
3255 parser.add_option('-i', dest='issue',
3256 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003257 parser.add_option('-j', '--json-file',
3258 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003259 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003260 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003261 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003262
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003263 issue = None
3264 if options.issue:
3265 try:
3266 issue = int(options.issue)
3267 except ValueError:
3268 DieWithError('A review issue id is expected to be a number')
3269
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003270 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003271
3272 if options.comment:
3273 cl.AddComment(options.comment)
3274 return 0
3275
3276 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003277 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003278 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003279 summary.append({
3280 'date': message['date'],
3281 'lgtm': False,
3282 'message': message['text'],
3283 'not_lgtm': False,
3284 'sender': message['sender'],
3285 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003286 if message['disapproval']:
3287 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003288 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003289 elif message['approval']:
3290 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003291 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003292 elif message['sender'] == data['owner_email']:
3293 color = Fore.MAGENTA
3294 else:
3295 color = Fore.BLUE
3296 print '\n%s%s %s%s' % (
3297 color, message['date'].split('.', 1)[0], message['sender'],
3298 Fore.RESET)
3299 if message['text'].strip():
3300 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00003301 if options.json_file:
3302 with open(options.json_file, 'wb') as f:
3303 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003304 return 0
3305
3306
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003307@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003308def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003309 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003310 parser.add_option('-d', '--display', action='store_true',
3311 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003312 parser.add_option('-n', '--new-description',
3313 help='New description to set for this issue (- for stdin)')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003314
3315 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003316 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003317 options, args = parser.parse_args(args)
3318 _process_codereview_select_options(parser, options)
3319
3320 target_issue = None
3321 if len(args) > 0:
3322 issue_arg = ParseIssueNumberArgument(args[0])
3323 if not issue_arg.valid:
3324 parser.print_help()
3325 return 1
3326 target_issue = issue_arg.issue
3327
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003328 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003329
3330 cl = Changelist(
3331 auth_config=auth_config, issue=target_issue,
3332 codereview=options.forced_codereview)
3333
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003334 if not cl.GetIssue():
3335 DieWithError('This branch has no associated changelist.')
3336 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003337
smut@google.com34fb6b12015-07-13 20:03:26 +00003338 if options.display:
tandrii@chromium.org8c3b4422016-04-27 13:11:18 +00003339 print description.description
smut@google.com34fb6b12015-07-13 20:03:26 +00003340 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003341
3342 if options.new_description:
3343 text = options.new_description
3344 if text == '-':
3345 text = '\n'.join(l.rstrip() for l in sys.stdin)
3346
3347 description.set_description(text)
3348 else:
3349 description.prompt()
3350
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003351 if cl.GetDescription() != description.description:
3352 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003353 return 0
3354
3355
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003356def CreateDescriptionFromLog(args):
3357 """Pulls out the commit log to use as a base for the CL description."""
3358 log_args = []
3359 if len(args) == 1 and not args[0].endswith('.'):
3360 log_args = [args[0] + '..']
3361 elif len(args) == 1 and args[0].endswith('...'):
3362 log_args = [args[0][:-1]]
3363 elif len(args) == 2:
3364 log_args = [args[0] + '..' + args[1]]
3365 else:
3366 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003367 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003368
3369
thestig@chromium.org44202a22014-03-11 19:22:18 +00003370def CMDlint(parser, args):
3371 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003372 parser.add_option('--filter', action='append', metavar='-x,+y',
3373 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003374 auth.add_auth_options(parser)
3375 options, args = parser.parse_args(args)
3376 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003377
3378 # Access to a protected member _XX of a client class
3379 # pylint: disable=W0212
3380 try:
3381 import cpplint
3382 import cpplint_chromium
3383 except ImportError:
3384 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
3385 return 1
3386
3387 # Change the current working directory before calling lint so that it
3388 # shows the correct base.
3389 previous_cwd = os.getcwd()
3390 os.chdir(settings.GetRoot())
3391 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003392 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003393 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3394 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003395 if not files:
3396 print "Cannot lint an empty CL"
3397 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003398
3399 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003400 command = args + files
3401 if options.filter:
3402 command = ['--filter=' + ','.join(options.filter)] + command
3403 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003404
3405 white_regex = re.compile(settings.GetLintRegex())
3406 black_regex = re.compile(settings.GetLintIgnoreRegex())
3407 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3408 for filename in filenames:
3409 if white_regex.match(filename):
3410 if black_regex.match(filename):
3411 print "Ignoring file %s" % filename
3412 else:
3413 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3414 extra_check_functions)
3415 else:
3416 print "Skipping file %s" % filename
3417 finally:
3418 os.chdir(previous_cwd)
3419 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
3420 if cpplint._cpplint_state.error_count != 0:
3421 return 1
3422 return 0
3423
3424
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003425def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003426 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003427 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003428 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003429 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003430 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003431 auth.add_auth_options(parser)
3432 options, args = parser.parse_args(args)
3433 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003434
sbc@chromium.org71437c02015-04-09 19:29:40 +00003435 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00003436 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003437 return 1
3438
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003439 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003440 if args:
3441 base_branch = args[0]
3442 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003443 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003444 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003445
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003446 cl.RunHook(
3447 committing=not options.upload,
3448 may_prompt=False,
3449 verbose=options.verbose,
3450 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003451 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003452
3453
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003454def GenerateGerritChangeId(message):
3455 """Returns Ixxxxxx...xxx change id.
3456
3457 Works the same way as
3458 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3459 but can be called on demand on all platforms.
3460
3461 The basic idea is to generate git hash of a state of the tree, original commit
3462 message, author/committer info and timestamps.
3463 """
3464 lines = []
3465 tree_hash = RunGitSilent(['write-tree'])
3466 lines.append('tree %s' % tree_hash.strip())
3467 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3468 if code == 0:
3469 lines.append('parent %s' % parent.strip())
3470 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3471 lines.append('author %s' % author.strip())
3472 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3473 lines.append('committer %s' % committer.strip())
3474 lines.append('')
3475 # Note: Gerrit's commit-hook actually cleans message of some lines and
3476 # whitespace. This code is not doing this, but it clearly won't decrease
3477 # entropy.
3478 lines.append(message)
3479 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3480 stdin='\n'.join(lines))
3481 return 'I%s' % change_hash.strip()
3482
3483
wittman@chromium.org455dc922015-01-26 20:15:50 +00003484def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3485 """Computes the remote branch ref to use for the CL.
3486
3487 Args:
3488 remote (str): The git remote for the CL.
3489 remote_branch (str): The git remote branch for the CL.
3490 target_branch (str): The target branch specified by the user.
3491 pending_prefix (str): The pending prefix from the settings.
3492 """
3493 if not (remote and remote_branch):
3494 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003495
wittman@chromium.org455dc922015-01-26 20:15:50 +00003496 if target_branch:
3497 # Cannonicalize branch references to the equivalent local full symbolic
3498 # refs, which are then translated into the remote full symbolic refs
3499 # below.
3500 if '/' not in target_branch:
3501 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3502 else:
3503 prefix_replacements = (
3504 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3505 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3506 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3507 )
3508 match = None
3509 for regex, replacement in prefix_replacements:
3510 match = re.search(regex, target_branch)
3511 if match:
3512 remote_branch = target_branch.replace(match.group(0), replacement)
3513 break
3514 if not match:
3515 # This is a branch path but not one we recognize; use as-is.
3516 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003517 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3518 # Handle the refs that need to land in different refs.
3519 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003520
wittman@chromium.org455dc922015-01-26 20:15:50 +00003521 # Create the true path to the remote branch.
3522 # Does the following translation:
3523 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3524 # * refs/remotes/origin/master -> refs/heads/master
3525 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3526 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3527 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3528 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3529 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3530 'refs/heads/')
3531 elif remote_branch.startswith('refs/remotes/branch-heads'):
3532 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3533 # If a pending prefix exists then replace refs/ with it.
3534 if pending_prefix:
3535 remote_branch = remote_branch.replace('refs/', pending_prefix)
3536 return remote_branch
3537
3538
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003539def cleanup_list(l):
3540 """Fixes a list so that comma separated items are put as individual items.
3541
3542 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3543 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3544 """
3545 items = sum((i.split(',') for i in l), [])
3546 stripped_items = (i.strip() for i in items)
3547 return sorted(filter(None, stripped_items))
3548
3549
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003550@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003551def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003552 """Uploads the current changelist to codereview.
3553
3554 Can skip dependency patchset uploads for a branch by running:
3555 git config branch.branch_name.skip-deps-uploads True
3556 To unset run:
3557 git config --unset branch.branch_name.skip-deps-uploads
3558 Can also set the above globally by using the --global flag.
3559 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003560 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3561 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003562 parser.add_option('--bypass-watchlists', action='store_true',
3563 dest='bypass_watchlists',
3564 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003565 parser.add_option('-f', action='store_true', dest='force',
3566 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003567 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003568 parser.add_option('-t', dest='title',
3569 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003570 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003571 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003572 help='reviewer email addresses')
3573 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003574 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003575 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003576 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003577 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003578 parser.add_option('--emulate_svn_auto_props',
3579 '--emulate-svn-auto-props',
3580 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003581 dest="emulate_svn_auto_props",
3582 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003583 parser.add_option('-c', '--use-commit-queue', action='store_true',
3584 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003585 parser.add_option('--private', action='store_true',
3586 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003587 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003588 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003589 metavar='TARGET',
3590 help='Apply CL to remote ref TARGET. ' +
3591 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003592 parser.add_option('--squash', action='store_true',
3593 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003594 parser.add_option('--no-squash', action='store_true',
3595 help='Don\'t squash multiple commits into one ' +
3596 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003597 parser.add_option('--email', default=None,
3598 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003599 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3600 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003601 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3602 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003603 help='Send the patchset to do a CQ dry run right after '
3604 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003605 parser.add_option('--dependencies', action='store_true',
3606 help='Uploads CLs of all the local branches that depend on '
3607 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003608
rmistry@google.com2dd99862015-06-22 12:22:18 +00003609 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003610 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003611 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003612 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003613 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003614 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003615 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003616
sbc@chromium.org71437c02015-04-09 19:29:40 +00003617 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003618 return 1
3619
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003620 options.reviewers = cleanup_list(options.reviewers)
3621 options.cc = cleanup_list(options.cc)
3622
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003623 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3624 settings.GetIsGerrit()
3625
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003626 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00003627 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003628
3629
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003630def IsSubmoduleMergeCommit(ref):
3631 # When submodules are added to the repo, we expect there to be a single
3632 # non-git-svn merge commit at remote HEAD with a signature comment.
3633 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003634 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003635 return RunGit(cmd) != ''
3636
3637
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003638def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003639 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003640
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003641 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3642 upstream and closes the issue automatically and atomically.
3643
3644 Otherwise (in case of Rietveld):
3645 Squashes branch into a single commit.
3646 Updates changelog with metadata (e.g. pointer to review).
3647 Pushes/dcommits the code upstream.
3648 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003649 """
3650 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3651 help='bypass upload presubmit hook')
3652 parser.add_option('-m', dest='message',
3653 help="override review description")
3654 parser.add_option('-f', action='store_true', dest='force',
3655 help="force yes to questions (don't prompt)")
3656 parser.add_option('-c', dest='contributor',
3657 help="external contributor for patch (appended to " +
3658 "description and used as author for git). Should be " +
3659 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003660 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003661 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003662 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003663 auth_config = auth.extract_auth_config_from_options(options)
3664
3665 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003666
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003667 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3668 if cl.IsGerrit():
3669 if options.message:
3670 # This could be implemented, but it requires sending a new patch to
3671 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3672 # Besides, Gerrit has the ability to change the commit message on submit
3673 # automatically, thus there is no need to support this option (so far?).
3674 parser.error('-m MESSAGE option is not supported for Gerrit.')
3675 if options.contributor:
3676 parser.error(
3677 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3678 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3679 'the contributor\'s "name <email>". If you can\'t upload such a '
3680 'commit for review, contact your repository admin and request'
3681 '"Forge-Author" permission.')
3682 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3683 options.verbose)
3684
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003685 current = cl.GetBranch()
3686 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3687 if not settings.GetIsGitSvn() and remote == '.':
3688 print
3689 print 'Attempting to push branch %r into another local branch!' % current
3690 print
3691 print 'Either reparent this branch on top of origin/master:'
3692 print ' git reparent-branch --root'
3693 print
3694 print 'OR run `git rebase-update` if you think the parent branch is already'
3695 print 'committed.'
3696 print
3697 print ' Current parent: %r' % upstream_branch
3698 return 1
3699
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003700 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003701 # Default to merging against our best guess of the upstream branch.
3702 args = [cl.GetUpstreamBranch()]
3703
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003704 if options.contributor:
3705 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
3706 print "Please provide contibutor as 'First Last <email@example.com>'"
3707 return 1
3708
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003709 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003710 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003711
sbc@chromium.org71437c02015-04-09 19:29:40 +00003712 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003713 return 1
3714
3715 # This rev-list syntax means "show all commits not in my branch that
3716 # are in base_branch".
3717 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3718 base_branch]).splitlines()
3719 if upstream_commits:
3720 print ('Base branch "%s" has %d commits '
3721 'not in this branch.' % (base_branch, len(upstream_commits)))
3722 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
3723 return 1
3724
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003725 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003726 svn_head = None
3727 if cmd == 'dcommit' or base_has_submodules:
3728 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
3729 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003730
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003731 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003732 # If the base_head is a submodule merge commit, the first parent of the
3733 # base_head should be a git-svn commit, which is what we're interested in.
3734 base_svn_head = base_branch
3735 if base_has_submodules:
3736 base_svn_head += '^1'
3737
3738 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003739 if extra_commits:
3740 print ('This branch has %d additional commits not upstreamed yet.'
3741 % len(extra_commits.splitlines()))
3742 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
3743 'before attempting to %s.' % (base_branch, cmd))
3744 return 1
3745
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003746 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003747 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003748 author = None
3749 if options.contributor:
3750 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003751 hook_results = cl.RunHook(
3752 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003753 may_prompt=not options.force,
3754 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003755 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003756 if not hook_results.should_continue():
3757 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003758
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003759 # Check the tree status if the tree status URL is set.
3760 status = GetTreeStatus()
3761 if 'closed' == status:
3762 print('The tree is closed. Please wait for it to reopen. Use '
3763 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3764 return 1
3765 elif 'unknown' == status:
3766 print('Unable to determine tree status. Please verify manually and '
3767 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3768 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003769
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003770 change_desc = ChangeDescription(options.message)
3771 if not change_desc.description and cl.GetIssue():
3772 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003773
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003774 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00003775 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003776 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00003777 else:
3778 print 'No description set.'
3779 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
3780 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003781
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003782 # Keep a separate copy for the commit message, because the commit message
3783 # contains the link to the Rietveld issue, while the Rietveld message contains
3784 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003785 # Keep a separate copy for the commit message.
3786 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00003787 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003788
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003789 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00003790 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00003791 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00003792 # after it. Add a period on a new line to circumvent this. Also add a space
3793 # before the period to make sure that Gitiles continues to correctly resolve
3794 # the URL.
3795 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003796 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003797 commit_desc.append_footer('Patch from %s.' % options.contributor)
3798
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00003799 print('Description:')
3800 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003801
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003802 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003803 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00003804 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003805
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003806 # We want to squash all this branch's commits into one commit with the proper
3807 # description. We do this by doing a "reset --soft" to the base branch (which
3808 # keeps the working copy the same), then dcommitting that. If origin/master
3809 # has a submodule merge commit, we'll also need to cherry-pick the squashed
3810 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003811 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003812 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
3813 # Delete the branches if they exist.
3814 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
3815 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
3816 result = RunGitWithCode(showref_cmd)
3817 if result[0] == 0:
3818 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003819
3820 # We might be in a directory that's present in this branch but not in the
3821 # trunk. Move up to the top of the tree so that git commands that expect a
3822 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003823 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003824 if rel_base_path:
3825 os.chdir(rel_base_path)
3826
3827 # Stuff our change into the merge branch.
3828 # We wrap in a try...finally block so if anything goes wrong,
3829 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003830 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003831 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003832 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003833 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003834 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00003835 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003836 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003837 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003838 RunGit(
3839 [
3840 'commit', '--author', options.contributor,
3841 '-m', commit_desc.description,
3842 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003843 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003844 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003845 if base_has_submodules:
3846 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
3847 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
3848 RunGit(['checkout', CHERRY_PICK_BRANCH])
3849 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003850 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003851 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003852 mirror = settings.GetGitMirror(remote)
3853 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003854 pending_prefix = settings.GetPendingRefPrefix()
3855 if not pending_prefix or branch.startswith(pending_prefix):
3856 # If not using refs/pending/heads/* at all, or target ref is already set
3857 # to pending, then push to the target ref directly.
3858 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003859 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003860 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003861 else:
3862 # Cherry-pick the change on top of pending ref and then push it.
3863 assert branch.startswith('refs/'), branch
3864 assert pending_prefix[-1] == '/', pending_prefix
3865 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003866 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003867 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003868 if retcode == 0:
3869 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003870 else:
3871 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003872 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003873 'svn', 'dcommit',
3874 '-C%s' % options.similarity,
3875 '--no-rebase', '--rmdir',
3876 ]
3877 if settings.GetForceHttpsCommitUrl():
3878 # Allow forcing https commit URLs for some projects that don't allow
3879 # committing to http URLs (like Google Code).
3880 remote_url = cl.GetGitSvnRemoteUrl()
3881 if urlparse.urlparse(remote_url).scheme == 'http':
3882 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003883 cmd_args.append('--commit-url=%s' % remote_url)
3884 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003885 if 'Committed r' in output:
3886 revision = re.match(
3887 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
3888 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003889 finally:
3890 # And then swap back to the original branch and clean up.
3891 RunGit(['checkout', '-q', cl.GetBranch()])
3892 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003893 if base_has_submodules:
3894 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003895
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003896 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003897 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003898 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003899
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003900 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003901 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003902 try:
3903 revision = WaitForRealCommit(remote, revision, base_branch, branch)
3904 # We set pushed_to_pending to False, since it made it all the way to the
3905 # real ref.
3906 pushed_to_pending = False
3907 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003908 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003909
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003910 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003911 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003912 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003913 if not to_pending:
3914 if viewvc_url and revision:
3915 change_desc.append_footer(
3916 'Committed: %s%s' % (viewvc_url, revision))
3917 elif revision:
3918 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003919 print ('Closing issue '
3920 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003921 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003922 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003923 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00003924 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00003925 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00003926 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003927 if options.bypass_hooks:
3928 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
3929 else:
3930 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00003931 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003932 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003933
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003934 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003935 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
3936 print 'The commit is in the pending queue (%s).' % pending_ref
3937 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00003938 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003939 'footer.' % branch)
3940
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003941 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
3942 if os.path.isfile(hook):
3943 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003944
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003945 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003946
3947
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003948def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
3949 print
3950 print 'Waiting for commit to be landed on %s...' % real_ref
3951 print '(If you are impatient, you may Ctrl-C once without harm)'
3952 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
3953 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003954 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003955
3956 loop = 0
3957 while True:
3958 sys.stdout.write('fetching (%d)... \r' % loop)
3959 sys.stdout.flush()
3960 loop += 1
3961
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003962 if mirror:
3963 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003964 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
3965 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
3966 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
3967 for commit in commits.splitlines():
3968 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
3969 print 'Found commit on %s' % real_ref
3970 return commit
3971
3972 current_rev = to_rev
3973
3974
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003975def PushToGitPending(remote, pending_ref, upstream_ref):
3976 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
3977
3978 Returns:
3979 (retcode of last operation, output log of last operation).
3980 """
3981 assert pending_ref.startswith('refs/'), pending_ref
3982 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
3983 cherry = RunGit(['rev-parse', 'HEAD']).strip()
3984 code = 0
3985 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003986 max_attempts = 3
3987 attempts_left = max_attempts
3988 while attempts_left:
3989 if attempts_left != max_attempts:
3990 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
3991 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003992
3993 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003994 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003995 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003996 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003997 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003998 print 'Fetch failed with exit code %d.' % code
3999 if out.strip():
4000 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004001 continue
4002
4003 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004004 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004005 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004006 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004007 if code:
4008 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004009 'Your patch doesn\'t apply cleanly to ref \'%s\', '
4010 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004011 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
4012 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004013 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004014 return code, out
4015
4016 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004017 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004018 code, out = RunGitWithCode(
4019 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4020 if code == 0:
4021 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004022 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004023 return code, out
4024
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004025 print 'Push failed with exit code %d.' % code
4026 if out.strip():
4027 print out.strip()
4028 if IsFatalPushFailure(out):
4029 print (
4030 'Fatal push error. Make sure your .netrc credentials and git '
4031 'user.email are correct and you have push access to the repo.')
4032 return code, out
4033
4034 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004035 return code, out
4036
4037
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004038def IsFatalPushFailure(push_stdout):
4039 """True if retrying push won't help."""
4040 return '(prohibited by Gerrit)' in push_stdout
4041
4042
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004043@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004044def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004045 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004046 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004047 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004048 # If it looks like previous commits were mirrored with git-svn.
4049 message = """This repository appears to be a git-svn mirror, but no
4050upstream SVN master is set. You probably need to run 'git auto-svn' once."""
4051 else:
4052 message = """This doesn't appear to be an SVN repository.
4053If your project has a true, writeable git repository, you probably want to run
4054'git cl land' instead.
4055If your project has a git mirror of an upstream SVN master, you probably need
4056to run 'git svn init'.
4057
4058Using the wrong command might cause your commit to appear to succeed, and the
4059review to be closed, without actually landing upstream. If you choose to
4060proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004061 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004062 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004063 return SendUpstream(parser, args, 'dcommit')
4064
4065
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004066@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004067def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004068 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004069 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004070 print('This appears to be an SVN repository.')
4071 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004072 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004073 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004074 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004075
4076
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004077@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004078def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004079 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004080 parser.add_option('-b', dest='newbranch',
4081 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004082 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004083 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004084 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4085 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004086 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004087 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004088 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004089 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004090 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004091 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004092
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004093
4094 group = optparse.OptionGroup(
4095 parser,
4096 'Options for continuing work on the current issue uploaded from a '
4097 'different clone (e.g. different machine). Must be used independently '
4098 'from the other options. No issue number should be specified, and the '
4099 'branch must have an issue number associated with it')
4100 group.add_option('--reapply', action='store_true', dest='reapply',
4101 help='Reset the branch and reapply the issue.\n'
4102 'CAUTION: This will undo any local changes in this '
4103 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004104
4105 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004106 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004107 parser.add_option_group(group)
4108
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004109 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004110 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004111 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004112 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004113 auth_config = auth.extract_auth_config_from_options(options)
4114
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004115 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004116
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004117 issue_arg = None
4118 if options.reapply :
4119 if len(args) > 0:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004120 parser.error('--reapply implies no additional arguments.')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004121
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004122 issue_arg = cl.GetIssue()
4123 upstream = cl.GetUpstreamBranch()
4124 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004125 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004126
4127 RunGit(['reset', '--hard', upstream])
4128 if options.pull:
4129 RunGit(['pull'])
4130 else:
4131 if len(args) != 1:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004132 parser.error('Must specify issue number or url')
4133 issue_arg = args[0]
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004134
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004135 if not issue_arg:
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004136 parser.print_help()
4137 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004138
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004139 if cl.IsGerrit():
4140 if options.reject:
4141 parser.error('--reject is not supported with Gerrit codereview.')
4142 if options.nocommit:
4143 parser.error('--nocommit is not supported with Gerrit codereview.')
4144 if options.directory:
4145 parser.error('--directory is not supported with Gerrit codereview.')
4146
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004147 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004148 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004149 return 1
4150
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004151 if options.newbranch:
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004152 if options.reapply:
4153 parser.error("--reapply excludes any option other than --pull")
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004154 if options.force:
4155 RunGit(['branch', '-D', options.newbranch],
4156 stderr=subprocess2.PIPE, error_ok=True)
4157 RunGit(['checkout', '-b', options.newbranch,
4158 Changelist().GetUpstreamBranch()])
4159
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004160 return cl.CMDPatchIssue(issue_arg, options.reject, options.nocommit,
4161 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004162
4163
4164def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004165 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004166 # Provide a wrapper for git svn rebase to help avoid accidental
4167 # git svn dcommit.
4168 # It's the only command that doesn't use parser at all since we just defer
4169 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004170
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004171 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004172
4173
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004174def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004175 """Fetches the tree status and returns either 'open', 'closed',
4176 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004177 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004178 if url:
4179 status = urllib2.urlopen(url).read().lower()
4180 if status.find('closed') != -1 or status == '0':
4181 return 'closed'
4182 elif status.find('open') != -1 or status == '1':
4183 return 'open'
4184 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004185 return 'unset'
4186
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004187
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004188def GetTreeStatusReason():
4189 """Fetches the tree status from a json url and returns the message
4190 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004191 url = settings.GetTreeStatusUrl()
4192 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004193 connection = urllib2.urlopen(json_url)
4194 status = json.loads(connection.read())
4195 connection.close()
4196 return status['message']
4197
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004198
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004199def GetBuilderMaster(bot_list):
4200 """For a given builder, fetch the master from AE if available."""
4201 map_url = 'https://builders-map.appspot.com/'
4202 try:
4203 master_map = json.load(urllib2.urlopen(map_url))
4204 except urllib2.URLError as e:
4205 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4206 (map_url, e))
4207 except ValueError as e:
4208 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4209 if not master_map:
4210 return None, 'Failed to build master map.'
4211
4212 result_master = ''
4213 for bot in bot_list:
4214 builder = bot.split(':', 1)[0]
4215 master_list = master_map.get(builder, [])
4216 if not master_list:
4217 return None, ('No matching master for builder %s.' % builder)
4218 elif len(master_list) > 1:
4219 return None, ('The builder name %s exists in multiple masters %s.' %
4220 (builder, master_list))
4221 else:
4222 cur_master = master_list[0]
4223 if not result_master:
4224 result_master = cur_master
4225 elif result_master != cur_master:
4226 return None, 'The builders do not belong to the same master.'
4227 return result_master, None
4228
4229
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004230def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004231 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004232 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004233 status = GetTreeStatus()
4234 if 'unset' == status:
4235 print 'You must configure your tree status URL by running "git cl config".'
4236 return 2
4237
4238 print "The tree is %s" % status
4239 print
4240 print GetTreeStatusReason()
4241 if status != 'open':
4242 return 1
4243 return 0
4244
4245
maruel@chromium.org15192402012-09-06 12:38:29 +00004246def CMDtry(parser, args):
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004247 """Triggers try jobs through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00004248 group = optparse.OptionGroup(parser, "Try job options")
4249 group.add_option(
4250 "-b", "--bot", action="append",
4251 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
4252 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004253 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00004254 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004255 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00004256 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004257 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00004258 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004259 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004260 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004261 "-r", "--revision",
4262 help="Revision to use for the try job; default: the "
4263 "revision will be determined by the try server; see "
4264 "its waterfall for more info")
4265 group.add_option(
4266 "-c", "--clobber", action="store_true", default=False,
4267 help="Force a clobber before building; e.g. don't do an "
4268 "incremental build")
4269 group.add_option(
4270 "--project",
4271 help="Override which project to use. Projects are defined "
4272 "server-side to define what default bot set to use")
4273 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004274 "-p", "--property", dest="properties", action="append", default=[],
4275 help="Specify generic properties in the form -p key1=value1 -p "
4276 "key2=value2 etc (buildbucket only). The value will be treated as "
4277 "json if decodable, or as string otherwise.")
4278 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004279 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004280 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004281 "--use-rietveld", action="store_true", default=False,
4282 help="Use Rietveld to trigger try jobs.")
4283 group.add_option(
4284 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4285 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004286 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004287 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004288 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004289 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004290
machenbach@chromium.org45453142015-09-15 08:45:22 +00004291 if options.use_rietveld and options.properties:
4292 parser.error('Properties can only be specified with buildbucket')
4293
4294 # Make sure that all properties are prop=value pairs.
4295 bad_params = [x for x in options.properties if '=' not in x]
4296 if bad_params:
4297 parser.error('Got properties with missing "=": %s' % bad_params)
4298
maruel@chromium.org15192402012-09-06 12:38:29 +00004299 if args:
4300 parser.error('Unknown arguments: %s' % args)
4301
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004302 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004303 if not cl.GetIssue():
4304 parser.error('Need to upload first')
4305
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004306 if cl.IsGerrit():
4307 parser.error(
4308 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4309 'If your project has Commit Queue, dry run is a workaround:\n'
4310 ' git cl set-commit --dry-run')
4311 # Code below assumes Rietveld issue.
4312 # TODO(tandrii): actually implement for Gerrit http://crbug.com/599931.
4313
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004314 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004315 if props.get('closed'):
4316 parser.error('Cannot send tryjobs for a closed CL')
4317
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004318 if props.get('private'):
4319 parser.error('Cannot use trybots with private issue')
4320
maruel@chromium.org15192402012-09-06 12:38:29 +00004321 if not options.name:
4322 options.name = cl.GetBranch()
4323
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004324 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004325 options.master, err_msg = GetBuilderMaster(options.bot)
4326 if err_msg:
4327 parser.error('Tryserver master cannot be found because: %s\n'
4328 'Please manually specify the tryserver master'
4329 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004330
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004331 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004332 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004333 if not options.bot:
4334 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004335
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004336 # Get try masters from PRESUBMIT.py files.
4337 masters = presubmit_support.DoGetTryMasters(
4338 change,
4339 change.LocalPaths(),
4340 settings.GetRoot(),
4341 None,
4342 None,
4343 options.verbose,
4344 sys.stdout)
4345 if masters:
4346 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004347
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004348 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4349 options.bot = presubmit_support.DoGetTrySlaves(
4350 change,
4351 change.LocalPaths(),
4352 settings.GetRoot(),
4353 None,
4354 None,
4355 options.verbose,
4356 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004357
4358 if not options.bot:
4359 # Get try masters from cq.cfg if any.
4360 # TODO(tandrii): some (but very few) projects store cq.cfg in different
4361 # location.
4362 cq_cfg = os.path.join(change.RepositoryRoot(),
4363 'infra', 'config', 'cq.cfg')
4364 if os.path.exists(cq_cfg):
4365 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00004366 cq_masters = commit_queue.get_master_builder_map(
4367 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004368 for master, builders in cq_masters.iteritems():
4369 for builder in builders:
4370 # Skip presubmit builders, because these will fail without LGTM.
machenbach@chromium.org2403e802016-04-29 12:34:42 +00004371 masters.setdefault(master, {})[builder] = ['defaulttests']
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004372 if masters:
4373 return masters
4374
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004375 if not options.bot:
4376 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00004377
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004378 builders_and_tests = {}
4379 # TODO(machenbach): The old style command-line options don't support
4380 # multiple try masters yet.
4381 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4382 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4383
4384 for bot in old_style:
4385 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004386 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004387 elif ',' in bot:
4388 parser.error('Specify one bot per --bot flag')
4389 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004390 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004391
4392 for bot, tests in new_style:
4393 builders_and_tests.setdefault(bot, []).extend(tests)
4394
4395 # Return a master map with one master to be backwards compatible. The
4396 # master name defaults to an empty string, which will cause the master
4397 # not to be set on rietveld (deprecated).
4398 return {options.master: builders_and_tests}
4399
4400 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004401
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004402 for builders in masters.itervalues():
4403 if any('triggered' in b for b in builders):
4404 print >> sys.stderr, (
4405 'ERROR You are trying to send a job to a triggered bot. This type of'
4406 ' bot requires an\ninitial job from a parent (usually a builder). '
4407 'Instead send your job to the parent.\n'
4408 'Bot list: %s' % builders)
4409 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004410
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004411 patchset = cl.GetMostRecentPatchset()
4412 if patchset and patchset != cl.GetPatchset():
4413 print(
4414 '\nWARNING Mismatch between local config and server. Did a previous '
4415 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4416 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004417 if options.luci:
4418 trigger_luci_job(cl, masters, options)
4419 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004420 try:
4421 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4422 except BuildbucketResponseException as ex:
4423 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004424 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004425 except Exception as e:
4426 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4427 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
4428 e, stacktrace)
4429 return 1
4430 else:
4431 try:
4432 cl.RpcServer().trigger_distributed_try_jobs(
4433 cl.GetIssue(), patchset, options.name, options.clobber,
4434 options.revision, masters)
4435 except urllib2.HTTPError as e:
4436 if e.code == 404:
4437 print('404 from rietveld; '
4438 'did you mean to use "git try" instead of "git cl try"?')
4439 return 1
4440 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004441
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004442 for (master, builders) in sorted(masters.iteritems()):
4443 if master:
4444 print 'Master: %s' % master
4445 length = max(len(builder) for builder in builders)
4446 for builder in sorted(builders):
4447 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00004448 return 0
4449
4450
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004451def CMDtry_results(parser, args):
4452 group = optparse.OptionGroup(parser, "Try job results options")
4453 group.add_option(
4454 "-p", "--patchset", type=int, help="patchset number if not current.")
4455 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004456 "--print-master", action='store_true', help="print master name as well.")
4457 group.add_option(
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004458 "--color", action='store_true', default=setup_color.IS_TTY,
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004459 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004460 group.add_option(
4461 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4462 help="Host of buildbucket. The default host is %default.")
4463 parser.add_option_group(group)
4464 auth.add_auth_options(parser)
4465 options, args = parser.parse_args(args)
4466 if args:
4467 parser.error('Unrecognized args: %s' % ' '.join(args))
4468
4469 auth_config = auth.extract_auth_config_from_options(options)
4470 cl = Changelist(auth_config=auth_config)
4471 if not cl.GetIssue():
4472 parser.error('Need to upload first')
4473
4474 if not options.patchset:
4475 options.patchset = cl.GetMostRecentPatchset()
4476 if options.patchset and options.patchset != cl.GetPatchset():
4477 print(
4478 '\nWARNING Mismatch between local config and server. Did a previous '
4479 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4480 'Continuing using\npatchset %s.\n' % options.patchset)
4481 try:
4482 jobs = fetch_try_jobs(auth_config, cl, options)
4483 except BuildbucketResponseException as ex:
4484 print 'Buildbucket error: %s' % ex
4485 return 1
4486 except Exception as e:
4487 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4488 print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
4489 e, stacktrace)
4490 return 1
4491 print_tryjobs(options, jobs)
4492 return 0
4493
4494
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004495@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004496def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004497 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004498 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004499 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004500 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004501
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004502 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004503 if args:
4504 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004505 branch = cl.GetBranch()
4506 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004507 cl = Changelist()
4508 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004509
4510 # Clear configured merge-base, if there is one.
4511 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004512 else:
4513 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004514 return 0
4515
4516
thestig@chromium.org00858c82013-12-02 23:08:03 +00004517def CMDweb(parser, args):
4518 """Opens the current CL in the web browser."""
4519 _, args = parser.parse_args(args)
4520 if args:
4521 parser.error('Unrecognized args: %s' % ' '.join(args))
4522
4523 issue_url = Changelist().GetIssueURL()
4524 if not issue_url:
4525 print >> sys.stderr, 'ERROR No issue to open'
4526 return 1
4527
4528 webbrowser.open(issue_url)
4529 return 0
4530
4531
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004532def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004533 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004534 parser.add_option('-d', '--dry-run', action='store_true',
4535 help='trigger in dry run mode')
4536 parser.add_option('-c', '--clear', action='store_true',
4537 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004538 auth.add_auth_options(parser)
4539 options, args = parser.parse_args(args)
4540 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004541 if args:
4542 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004543 if options.dry_run and options.clear:
4544 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4545
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004546 cl = Changelist(auth_config=auth_config)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004547 if options.clear:
4548 state = _CQState.CLEAR
4549 elif options.dry_run:
4550 state = _CQState.DRY_RUN
4551 else:
4552 state = _CQState.COMMIT
4553 if not cl.GetIssue():
4554 parser.error('Must upload the issue first')
4555 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004556 return 0
4557
4558
groby@chromium.org411034a2013-02-26 15:12:01 +00004559def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004560 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004561 auth.add_auth_options(parser)
4562 options, args = parser.parse_args(args)
4563 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004564 if args:
4565 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004566 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00004567 # Ensure there actually is an issue to close.
4568 cl.GetDescription()
4569 cl.CloseIssue()
4570 return 0
4571
4572
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004573def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004574 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004575 auth.add_auth_options(parser)
4576 options, args = parser.parse_args(args)
4577 auth_config = auth.extract_auth_config_from_options(options)
4578 if args:
4579 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004580
4581 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004582 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004583 # Staged changes would be committed along with the patch from last
4584 # upload, hence counted toward the "last upload" side in the final
4585 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004586 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004587 return 1
4588
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004589 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004590 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004591 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004592 if not issue:
4593 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004594 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004595 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004596
4597 # Create a new branch based on the merge-base
4598 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004599 # Clear cached branch in cl object, to avoid overwriting original CL branch
4600 # properties.
4601 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004602 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004603 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004604 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004605 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004606 return rtn
4607
wychen@chromium.org06928532015-02-03 02:11:29 +00004608 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004609 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00004610 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004611 finally:
4612 RunGit(['checkout', '-q', branch])
4613 RunGit(['branch', '-D', TMP_BRANCH])
4614
4615 return 0
4616
4617
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004618def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004619 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004620 parser.add_option(
4621 '--no-color',
4622 action='store_true',
4623 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004624 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004625 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004626 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004627
4628 author = RunGit(['config', 'user.email']).strip() or None
4629
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004630 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004631
4632 if args:
4633 if len(args) > 1:
4634 parser.error('Unknown args')
4635 base_branch = args[0]
4636 else:
4637 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004638 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004639
4640 change = cl.GetChange(base_branch, None)
4641 return owners_finder.OwnersFinder(
4642 [f.LocalPath() for f in
4643 cl.GetChange(base_branch, None).AffectedFiles()],
4644 change.RepositoryRoot(), author,
4645 fopen=file, os_path=os.path, glob=glob.glob,
4646 disable_color=options.no_color).run()
4647
4648
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004649def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004650 """Generates a diff command."""
4651 # Generate diff for the current branch's changes.
4652 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4653 upstream_commit, '--' ]
4654
4655 if args:
4656 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004657 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004658 diff_cmd.append(arg)
4659 else:
4660 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004661
4662 return diff_cmd
4663
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004664def MatchingFileType(file_name, extensions):
4665 """Returns true if the file name ends with one of the given extensions."""
4666 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004667
enne@chromium.org555cfe42014-01-29 18:21:39 +00004668@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004669def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004670 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00004671 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004672 GN_EXTS = ['.gn', '.gni']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004673 parser.add_option('--full', action='store_true',
4674 help='Reformat the full content of all touched files')
4675 parser.add_option('--dry-run', action='store_true',
4676 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004677 parser.add_option('--python', action='store_true',
4678 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004679 parser.add_option('--diff', action='store_true',
4680 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004681 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004682
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004683 # git diff generates paths against the root of the repository. Change
4684 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004685 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004686 if rel_base_path:
4687 os.chdir(rel_base_path)
4688
digit@chromium.org29e47272013-05-17 17:01:46 +00004689 # Grab the merge-base commit, i.e. the upstream commit of the current
4690 # branch when it was created or the last time it was rebased. This is
4691 # to cover the case where the user may have called "git fetch origin",
4692 # moving the origin branch to a newer commit, but hasn't rebased yet.
4693 upstream_commit = None
4694 cl = Changelist()
4695 upstream_branch = cl.GetUpstreamBranch()
4696 if upstream_branch:
4697 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4698 upstream_commit = upstream_commit.strip()
4699
4700 if not upstream_commit:
4701 DieWithError('Could not find base commit for this branch. '
4702 'Are you in detached state?')
4703
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004704 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4705 diff_output = RunGit(changed_files_cmd)
4706 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004707 # Filter out files deleted by this CL
4708 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004709
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004710 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4711 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4712 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004713 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004714
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004715 top_dir = os.path.normpath(
4716 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
4717
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004718 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4719 # formatted. This is used to block during the presubmit.
4720 return_value = 0
4721
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004722 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004723 # Locate the clang-format binary in the checkout
4724 try:
4725 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
4726 except clang_format.NotFoundError, e:
4727 DieWithError(e)
4728
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004729 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004730 cmd = [clang_format_tool]
4731 if not opts.dry_run and not opts.diff:
4732 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004733 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004734 if opts.diff:
4735 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004736 else:
4737 env = os.environ.copy()
4738 env['PATH'] = str(os.path.dirname(clang_format_tool))
4739 try:
4740 script = clang_format.FindClangFormatScriptInChromiumTree(
4741 'clang-format-diff.py')
4742 except clang_format.NotFoundError, e:
4743 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004744
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004745 cmd = [sys.executable, script, '-p0']
4746 if not opts.dry_run and not opts.diff:
4747 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004748
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004749 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
4750 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004751
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004752 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
4753 if opts.diff:
4754 sys.stdout.write(stdout)
4755 if opts.dry_run and len(stdout) > 0:
4756 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004757
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004758 # Similar code to above, but using yapf on .py files rather than clang-format
4759 # on C/C++ files
4760 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004761 yapf_tool = gclient_utils.FindExecutable('yapf')
4762 if yapf_tool is None:
4763 DieWithError('yapf not found in PATH')
4764
4765 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004766 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004767 cmd = [yapf_tool]
4768 if not opts.dry_run and not opts.diff:
4769 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004770 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004771 if opts.diff:
4772 sys.stdout.write(stdout)
4773 else:
4774 # TODO(sbc): yapf --lines mode still has some issues.
4775 # https://github.com/google/yapf/issues/154
4776 DieWithError('--python currently only works with --full')
4777
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004778 # Dart's formatter does not have the nice property of only operating on
4779 # modified chunks, so hard code full.
4780 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004781 try:
4782 command = [dart_format.FindDartFmtToolInChromiumTree()]
4783 if not opts.dry_run and not opts.diff:
4784 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004785 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004786
ppi@chromium.org6593d932016-03-03 15:41:15 +00004787 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004788 if opts.dry_run and stdout:
4789 return_value = 2
4790 except dart_format.NotFoundError as e:
erikcorry@chromium.org3e445022015-12-17 09:07:26 +00004791 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
4792 'found in this checkout. Files in other languages are still ' +
4793 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004794
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004795 # Format GN build files. Always run on full build files for canonical form.
4796 if gn_diff_files:
4797 cmd = ['gn', 'format']
4798 if not opts.dry_run and not opts.diff:
4799 cmd.append('--in-place')
4800 for gn_diff_file in gn_diff_files:
bsep@chromium.org627d9002016-04-29 00:00:52 +00004801 stdout = RunCommand(cmd + [gn_diff_file],
4802 shell=sys.platform == 'win32',
4803 cwd=top_dir)
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004804 if opts.diff:
4805 sys.stdout.write(stdout)
4806
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004807 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004808
4809
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004810@subcommand.usage('<codereview url or issue id>')
4811def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004812 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004813 _, args = parser.parse_args(args)
4814
4815 if len(args) != 1:
4816 parser.print_help()
4817 return 1
4818
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004819 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00004820 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004821 parser.print_help()
4822 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00004823 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004824
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004825 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00004826 output = RunGit(['config', '--local', '--get-regexp',
4827 r'branch\..*\.%s' % issueprefix],
4828 error_ok=True)
4829 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004830 if issue == target_issue:
4831 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004832
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004833 branches = []
4834 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00004835 branches.extend(find_issues(cls.IssueSettingSuffix()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004836 if len(branches) == 0:
4837 print 'No branch found for issue %s.' % target_issue
4838 return 1
4839 if len(branches) == 1:
4840 RunGit(['checkout', branches[0]])
4841 else:
4842 print 'Multiple branches match issue %s:' % target_issue
4843 for i in range(len(branches)):
4844 print '%d: %s' % (i, branches[i])
4845 which = raw_input('Choose by index: ')
4846 try:
4847 RunGit(['checkout', branches[int(which)]])
4848 except (IndexError, ValueError):
4849 print 'Invalid selection, not checking out any branch.'
4850 return 1
4851
4852 return 0
4853
4854
maruel@chromium.org29404b52014-09-08 22:58:00 +00004855def CMDlol(parser, args):
4856 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00004857 print zlib.decompress(base64.b64decode(
4858 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
4859 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
4860 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
4861 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00004862 return 0
4863
4864
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004865class OptionParser(optparse.OptionParser):
4866 """Creates the option parse and add --verbose support."""
4867 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004868 optparse.OptionParser.__init__(
4869 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004870 self.add_option(
4871 '-v', '--verbose', action='count', default=0,
4872 help='Use 2 times for more debugging info')
4873
4874 def parse_args(self, args=None, values=None):
4875 options, args = optparse.OptionParser.parse_args(self, args, values)
4876 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
4877 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
4878 return options, args
4879
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004880
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004881def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00004882 if sys.hexversion < 0x02060000:
4883 print >> sys.stderr, (
4884 '\nYour python version %s is unsupported, please upgrade.\n' %
4885 sys.version.split(' ', 1)[0])
4886 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004887
maruel@chromium.orgddd59412011-11-30 14:20:38 +00004888 # Reload settings.
4889 global settings
4890 settings = Settings()
4891
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004892 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004893 dispatcher = subcommand.CommandDispatcher(__name__)
4894 try:
4895 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00004896 except auth.AuthenticationError as e:
4897 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004898 except urllib2.HTTPError, e:
4899 if e.code != 500:
4900 raise
4901 DieWithError(
4902 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
4903 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00004904 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004905
4906
4907if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004908 # These affect sys.stdout so do it outside of main() to simplify mocks in
4909 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00004910 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004911 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00004912 try:
4913 sys.exit(main(sys.argv[1:]))
4914 except KeyboardInterrupt:
4915 sys.stderr.write('interrupted\n')
4916 sys.exit(1)