blob: eaaed167aff5f4121c58a9aaf0814339ac337371 [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
2141 if data['status'] == 'ABANDONED':
2142 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.orgaa6235b2016-04-11 21:35:29 +00002471 cc = self.GetCCList().split(',')
2472 if options.cc:
2473 cc.extend(options.cc)
2474 cc = filter(None, cc)
2475 if cc:
tandrii@chromium.org0b2d7072016-04-18 16:19:03 +00002476 # refspec_opts.extend('cc=' + email.strip() for email in cc)
2477 # TODO(tandrii): enable this back. http://crbug.com/604377
2478 print('WARNING: Gerrit doesn\'t yet support cc-ing arbitrary emails.\n'
2479 ' Ignoring cc-ed emails. See http://crbug.com/604377.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002480
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002481 if change_desc.get_reviewers():
2482 refspec_opts.extend('r=' + email.strip()
2483 for email in change_desc.get_reviewers())
2484
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002485
2486 refspec_suffix = ''
2487 if refspec_opts:
2488 refspec_suffix = '%' + ','.join(refspec_opts)
2489 assert ' ' not in refspec_suffix, (
2490 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002491 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002492
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002493 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002494 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002495 print_stdout=True,
2496 # Flush after every line: useful for seeing progress when running as
2497 # recipe.
2498 filter_fn=lambda _: sys.stdout.flush())
2499
2500 if options.squash:
2501 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2502 change_numbers = [m.group(1)
2503 for m in map(regex.match, push_stdout.splitlines())
2504 if m]
2505 if len(change_numbers) != 1:
2506 DieWithError(
2507 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2508 'Change-Id: %s') % (len(change_numbers), change_id))
2509 self.SetIssue(change_numbers[0])
2510 RunGit(['config', 'branch.%s.gerritsquashhash' % self.GetBranch(),
2511 ref_to_push])
2512 return 0
2513
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002514 def _AddChangeIdToCommitMessage(self, options, args):
2515 """Re-commits using the current message, assumes the commit hook is in
2516 place.
2517 """
2518 log_desc = options.message or CreateDescriptionFromLog(args)
2519 git_command = ['commit', '--amend', '-m', log_desc]
2520 RunGit(git_command)
2521 new_log_desc = CreateDescriptionFromLog(args)
2522 if git_footers.get_footer_change_id(new_log_desc):
2523 print 'git-cl: Added Change-Id to commit message.'
2524 return new_log_desc
2525 else:
2526 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002527
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002528 def SetCQState(self, new_state):
2529 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
2530 # TODO(tandrii): maybe allow configurability in codereview.settings or by
2531 # self-discovery of label config for this CL using REST API.
2532 vote_map = {
2533 _CQState.NONE: 0,
2534 _CQState.DRY_RUN: 1,
2535 _CQState.COMMIT : 2,
2536 }
2537 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2538 labels={'Commit-Queue': vote_map[new_state]})
2539
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002540
2541_CODEREVIEW_IMPLEMENTATIONS = {
2542 'rietveld': _RietveldChangelistImpl,
2543 'gerrit': _GerritChangelistImpl,
2544}
2545
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002546
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002547def _add_codereview_select_options(parser):
2548 """Appends --gerrit and --rietveld options to force specific codereview."""
2549 parser.codereview_group = optparse.OptionGroup(
2550 parser, 'EXPERIMENTAL! Codereview override options')
2551 parser.add_option_group(parser.codereview_group)
2552 parser.codereview_group.add_option(
2553 '--gerrit', action='store_true',
2554 help='Force the use of Gerrit for codereview')
2555 parser.codereview_group.add_option(
2556 '--rietveld', action='store_true',
2557 help='Force the use of Rietveld for codereview')
2558
2559
2560def _process_codereview_select_options(parser, options):
2561 if options.gerrit and options.rietveld:
2562 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2563 options.forced_codereview = None
2564 if options.gerrit:
2565 options.forced_codereview = 'gerrit'
2566 elif options.rietveld:
2567 options.forced_codereview = 'rietveld'
2568
2569
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002570class ChangeDescription(object):
2571 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002572 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002573 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002574
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002575 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002576 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002577
agable@chromium.org42c20792013-09-12 17:34:49 +00002578 @property # www.logilab.org/ticket/89786
2579 def description(self): # pylint: disable=E0202
2580 return '\n'.join(self._description_lines)
2581
2582 def set_description(self, desc):
2583 if isinstance(desc, basestring):
2584 lines = desc.splitlines()
2585 else:
2586 lines = [line.rstrip() for line in desc]
2587 while lines and not lines[0]:
2588 lines.pop(0)
2589 while lines and not lines[-1]:
2590 lines.pop(-1)
2591 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002592
piman@chromium.org336f9122014-09-04 02:16:55 +00002593 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002594 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002595 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002596 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002597 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002598 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002599
agable@chromium.org42c20792013-09-12 17:34:49 +00002600 # Get the set of R= and TBR= lines and remove them from the desciption.
2601 regexp = re.compile(self.R_LINE)
2602 matches = [regexp.match(line) for line in self._description_lines]
2603 new_desc = [l for i, l in enumerate(self._description_lines)
2604 if not matches[i]]
2605 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002606
agable@chromium.org42c20792013-09-12 17:34:49 +00002607 # Construct new unified R= and TBR= lines.
2608 r_names = []
2609 tbr_names = []
2610 for match in matches:
2611 if not match:
2612 continue
2613 people = cleanup_list([match.group(2).strip()])
2614 if match.group(1) == 'TBR':
2615 tbr_names.extend(people)
2616 else:
2617 r_names.extend(people)
2618 for name in r_names:
2619 if name not in reviewers:
2620 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002621 if add_owners_tbr:
2622 owners_db = owners.Database(change.RepositoryRoot(),
2623 fopen=file, os_path=os.path, glob=glob.glob)
2624 all_reviewers = set(tbr_names + reviewers)
2625 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2626 all_reviewers)
2627 tbr_names.extend(owners_db.reviewers_for(missing_files,
2628 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002629 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2630 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2631
2632 # Put the new lines in the description where the old first R= line was.
2633 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2634 if 0 <= line_loc < len(self._description_lines):
2635 if new_tbr_line:
2636 self._description_lines.insert(line_loc, new_tbr_line)
2637 if new_r_line:
2638 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002639 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002640 if new_r_line:
2641 self.append_footer(new_r_line)
2642 if new_tbr_line:
2643 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002644
2645 def prompt(self):
2646 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002647 self.set_description([
2648 '# Enter a description of the change.',
2649 '# This will be displayed on the codereview site.',
2650 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002651 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002652 '--------------------',
2653 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002654
agable@chromium.org42c20792013-09-12 17:34:49 +00002655 regexp = re.compile(self.BUG_LINE)
2656 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00002657 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00002658 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002659 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002660 if not content:
2661 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002662 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002663
2664 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002665 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2666 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002667 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002668 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002669
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002670 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00002671 if self._description_lines:
2672 # Add an empty line if either the last line or the new line isn't a tag.
2673 last_line = self._description_lines[-1]
2674 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
2675 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2676 self._description_lines.append('')
2677 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002678
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002679 def get_reviewers(self):
2680 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002681 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
2682 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002683 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002684
2685
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002686def get_approving_reviewers(props):
2687 """Retrieves the reviewers that approved a CL from the issue properties with
2688 messages.
2689
2690 Note that the list may contain reviewers that are not committer, thus are not
2691 considered by the CQ.
2692 """
2693 return sorted(
2694 set(
2695 message['sender']
2696 for message in props['messages']
2697 if message['approval'] and message['sender'] in props['reviewers']
2698 )
2699 )
2700
2701
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002702def FindCodereviewSettingsFile(filename='codereview.settings'):
2703 """Finds the given file starting in the cwd and going up.
2704
2705 Only looks up to the top of the repository unless an
2706 'inherit-review-settings-ok' file exists in the root of the repository.
2707 """
2708 inherit_ok_file = 'inherit-review-settings-ok'
2709 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002710 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002711 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2712 root = '/'
2713 while True:
2714 if filename in os.listdir(cwd):
2715 if os.path.isfile(os.path.join(cwd, filename)):
2716 return open(os.path.join(cwd, filename))
2717 if cwd == root:
2718 break
2719 cwd = os.path.dirname(cwd)
2720
2721
2722def LoadCodereviewSettingsFromFile(fileobj):
2723 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002724 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002725
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002726 def SetProperty(name, setting, unset_error_ok=False):
2727 fullname = 'rietveld.' + name
2728 if setting in keyvals:
2729 RunGit(['config', fullname, keyvals[setting]])
2730 else:
2731 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2732
2733 SetProperty('server', 'CODE_REVIEW_SERVER')
2734 # Only server setting is required. Other settings can be absent.
2735 # In that case, we ignore errors raised during option deletion attempt.
2736 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002737 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002738 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2739 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002740 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002741 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002742 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2743 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002744 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002745 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002746 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002747 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2748 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002749
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002750 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002751 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002752
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002753 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
2754 RunGit(['config', 'gerrit.squash-uploads',
2755 keyvals['GERRIT_SQUASH_UPLOADS']])
2756
tandrii@chromium.org28253532016-04-14 13:46:56 +00002757 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00002758 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00002759 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
2760
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002761 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2762 #should be of the form
2763 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2764 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2765 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2766 keyvals['ORIGIN_URL_CONFIG']])
2767
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002768
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002769def urlretrieve(source, destination):
2770 """urllib is broken for SSL connections via a proxy therefore we
2771 can't use urllib.urlretrieve()."""
2772 with open(destination, 'w') as f:
2773 f.write(urllib2.urlopen(source).read())
2774
2775
ukai@chromium.org712d6102013-11-27 00:52:58 +00002776def hasSheBang(fname):
2777 """Checks fname is a #! script."""
2778 with open(fname) as f:
2779 return f.read(2).startswith('#!')
2780
2781
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00002782# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
2783def DownloadHooks(*args, **kwargs):
2784 pass
2785
2786
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002787def DownloadGerritHook(force):
2788 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002789
2790 Args:
2791 force: True to update hooks. False to install hooks if not present.
2792 """
2793 if not settings.GetIsGerrit():
2794 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00002795 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002796 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2797 if not os.access(dst, os.X_OK):
2798 if os.path.exists(dst):
2799 if not force:
2800 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002801 try:
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002802 print(
2803 'WARNING: installing Gerrit commit-msg hook.\n'
2804 ' This behavior of git cl will soon be disabled.\n'
2805 ' See bug http://crbug.com/579176.')
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002806 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002807 if not hasSheBang(dst):
2808 DieWithError('Not a script: %s\n'
2809 'You need to download from\n%s\n'
2810 'into .git/hooks/commit-msg and '
2811 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002812 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2813 except Exception:
2814 if os.path.exists(dst):
2815 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002816 DieWithError('\nFailed to download hooks.\n'
2817 'You need to download from\n%s\n'
2818 'into .git/hooks/commit-msg and '
2819 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002820
2821
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002822
2823def GetRietveldCodereviewSettingsInteractively():
2824 """Prompt the user for settings."""
2825 server = settings.GetDefaultServerUrl(error_ok=True)
2826 prompt = 'Rietveld server (host[:port])'
2827 prompt += ' [%s]' % (server or DEFAULT_SERVER)
2828 newserver = ask_for_data(prompt + ':')
2829 if not server and not newserver:
2830 newserver = DEFAULT_SERVER
2831 if newserver:
2832 newserver = gclient_utils.UpgradeToHttps(newserver)
2833 if newserver != server:
2834 RunGit(['config', 'rietveld.server', newserver])
2835
2836 def SetProperty(initial, caption, name, is_url):
2837 prompt = caption
2838 if initial:
2839 prompt += ' ("x" to clear) [%s]' % initial
2840 new_val = ask_for_data(prompt + ':')
2841 if new_val == 'x':
2842 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
2843 elif new_val:
2844 if is_url:
2845 new_val = gclient_utils.UpgradeToHttps(new_val)
2846 if new_val != initial:
2847 RunGit(['config', 'rietveld.' + name, new_val])
2848
2849 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
2850 SetProperty(settings.GetDefaultPrivateFlag(),
2851 'Private flag (rietveld only)', 'private', False)
2852 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
2853 'tree-status-url', False)
2854 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
2855 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
2856 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
2857 'run-post-upload-hook', False)
2858
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002859@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002860def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002861 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002862
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002863 print('WARNING: git cl config works for Rietveld only.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002864 'For Gerrit, see http://crbug.com/603116.')
2865 # TODO(tandrii): add Gerrit support as part of http://crbug.com/603116.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00002866 parser.add_option('--activate-update', action='store_true',
2867 help='activate auto-updating [rietveld] section in '
2868 '.git/config')
2869 parser.add_option('--deactivate-update', action='store_true',
2870 help='deactivate auto-updating [rietveld] section in '
2871 '.git/config')
2872 options, args = parser.parse_args(args)
2873
2874 if options.deactivate_update:
2875 RunGit(['config', 'rietveld.autoupdate', 'false'])
2876 return
2877
2878 if options.activate_update:
2879 RunGit(['config', '--unset', 'rietveld.autoupdate'])
2880 return
2881
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002882 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002883 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002884 return 0
2885
2886 url = args[0]
2887 if not url.endswith('codereview.settings'):
2888 url = os.path.join(url, 'codereview.settings')
2889
2890 # Load code review settings and download hooks (if available).
2891 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
2892 return 0
2893
2894
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002895def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002896 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002897 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
2898 branch = ShortBranchName(branchref)
2899 _, args = parser.parse_args(args)
2900 if not args:
2901 print("Current base-url:")
2902 return RunGit(['config', 'branch.%s.base-url' % branch],
2903 error_ok=False).strip()
2904 else:
2905 print("Setting base-url to %s" % args[0])
2906 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
2907 error_ok=False).strip()
2908
2909
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002910def color_for_status(status):
2911 """Maps a Changelist status to color, for CMDstatus and other tools."""
2912 return {
2913 'unsent': Fore.RED,
2914 'waiting': Fore.BLUE,
2915 'reply': Fore.YELLOW,
2916 'lgtm': Fore.GREEN,
2917 'commit': Fore.MAGENTA,
2918 'closed': Fore.CYAN,
2919 'error': Fore.WHITE,
2920 }.get(status, Fore.WHITE)
2921
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00002922def fetch_cl_status(branch, auth_config=None):
2923 """Fetches information for an issue and returns (branch, issue, status)."""
2924 cl = Changelist(branchref=branch, auth_config=auth_config)
2925 url = cl.GetIssueURL()
2926 status = cl.GetStatus()
2927
2928 if url and (not status or status == 'error'):
2929 # The issue probably doesn't exist anymore.
2930 url += ' (broken)'
2931
2932 return (branch, url, status)
2933
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002934def get_cl_statuses(
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00002935 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgcf197482016-04-29 20:15:53 +00002936 """Returns a blocking iterable of (branch, issue, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002937
2938 If fine_grained is true, this will fetch CL statuses from the server.
2939 Otherwise, simply indicate if there's a matching url for the given branches.
2940
2941 If max_processes is specified, it is used as the maximum number of processes
2942 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
2943 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00002944
2945 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002946 """
calamity@chromium.orgcf197482016-04-29 20:15:53 +00002947 def fetch(branch):
2948 if not branch:
2949 return None
2950
2951 return fetch_cl_status(branch, auth_config=auth_config)
2952
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002953 # Silence upload.py otherwise it becomes unwieldly.
2954 upload.verbosity = 0
2955
2956 if fine_grained:
2957 # Process one branch synchronously to work through authentication, then
2958 # spawn processes to process all the other branches in parallel.
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00002959 if branches:
calamity@chromium.orgcf197482016-04-29 20:15:53 +00002960
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00002961 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002962
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00002963 branches_to_fetch = branches[1:]
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002964 pool = ThreadPool(
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00002965 min(max_processes, len(branches_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002966 if max_processes is not None
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00002967 else len(branches_to_fetch))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00002968
2969 fetched_branches = set()
2970 it = pool.imap_unordered(fetch, branches_to_fetch).__iter__()
2971 while True:
2972 try:
2973 row = it.next(timeout=5)
2974 except multiprocessing.TimeoutError:
2975 break
2976
2977 fetched_branches.add(row[0])
2978 yield row
2979
2980 # Add any branches that failed to fetch.
2981 for b in set(branches_to_fetch) - fetched_branches:
2982 cl = Changelist(branchref=b, auth_config=auth_config)
2983 yield (b, cl.GetIssueURL() if b else None, 'error')
2984
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002985 else:
2986 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00002987 for b in branches:
2988 cl = Changelist(branchref=b, auth_config=auth_config)
calamity@chromium.orgcf197482016-04-29 20:15:53 +00002989 url = cl.GetIssueURL() if b else None
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00002990 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002991
rmistry@google.com2dd99862015-06-22 12:22:18 +00002992
2993def upload_branch_deps(cl, args):
2994 """Uploads CLs of local branches that are dependents of the current branch.
2995
2996 If the local branch dependency tree looks like:
2997 test1 -> test2.1 -> test3.1
2998 -> test3.2
2999 -> test2.2 -> test3.3
3000
3001 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3002 run on the dependent branches in this order:
3003 test2.1, test3.1, test3.2, test2.2, test3.3
3004
3005 Note: This function does not rebase your local dependent branches. Use it when
3006 you make a change to the parent branch that will not conflict with its
3007 dependent branches, and you would like their dependencies updated in
3008 Rietveld.
3009 """
3010 if git_common.is_dirty_git_tree('upload-branch-deps'):
3011 return 1
3012
3013 root_branch = cl.GetBranch()
3014 if root_branch is None:
3015 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3016 'Get on a branch!')
3017 if not cl.GetIssue() or not cl.GetPatchset():
3018 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3019 'patchset dependencies without an uploaded CL.')
3020
3021 branches = RunGit(['for-each-ref',
3022 '--format=%(refname:short) %(upstream:short)',
3023 'refs/heads'])
3024 if not branches:
3025 print('No local branches found.')
3026 return 0
3027
3028 # Create a dictionary of all local branches to the branches that are dependent
3029 # on it.
3030 tracked_to_dependents = collections.defaultdict(list)
3031 for b in branches.splitlines():
3032 tokens = b.split()
3033 if len(tokens) == 2:
3034 branch_name, tracked = tokens
3035 tracked_to_dependents[tracked].append(branch_name)
3036
3037 print
3038 print 'The dependent local branches of %s are:' % root_branch
3039 dependents = []
3040 def traverse_dependents_preorder(branch, padding=''):
3041 dependents_to_process = tracked_to_dependents.get(branch, [])
3042 padding += ' '
3043 for dependent in dependents_to_process:
3044 print '%s%s' % (padding, dependent)
3045 dependents.append(dependent)
3046 traverse_dependents_preorder(dependent, padding)
3047 traverse_dependents_preorder(root_branch)
3048 print
3049
3050 if not dependents:
3051 print 'There are no dependent local branches for %s' % root_branch
3052 return 0
3053
3054 print ('This command will checkout all dependent branches and run '
3055 '"git cl upload".')
3056 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3057
andybons@chromium.org962f9462016-02-03 20:00:42 +00003058 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003059 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003060 args.extend(['-t', 'Updated patchset dependency'])
3061
rmistry@google.com2dd99862015-06-22 12:22:18 +00003062 # Record all dependents that failed to upload.
3063 failures = {}
3064 # Go through all dependents, checkout the branch and upload.
3065 try:
3066 for dependent_branch in dependents:
3067 print
3068 print '--------------------------------------'
3069 print 'Running "git cl upload" from %s:' % dependent_branch
3070 RunGit(['checkout', '-q', dependent_branch])
3071 print
3072 try:
3073 if CMDupload(OptionParser(), args) != 0:
3074 print 'Upload failed for %s!' % dependent_branch
3075 failures[dependent_branch] = 1
3076 except: # pylint: disable=W0702
3077 failures[dependent_branch] = 1
3078 print
3079 finally:
3080 # Swap back to the original root branch.
3081 RunGit(['checkout', '-q', root_branch])
3082
3083 print
3084 print 'Upload complete for dependent branches!'
3085 for dependent_branch in dependents:
3086 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
3087 print ' %s : %s' % (dependent_branch, upload_status)
3088 print
3089
3090 return 0
3091
3092
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003093def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003094 """Show status of changelists.
3095
3096 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003097 - Red not sent for review or broken
3098 - Blue waiting for review
3099 - Yellow waiting for you to reply to review
3100 - Green LGTM'ed
3101 - Magenta in the commit queue
3102 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003103
3104 Also see 'git cl comments'.
3105 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003106 parser.add_option('--field',
3107 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003108 parser.add_option('-f', '--fast', action='store_true',
3109 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003110 parser.add_option(
3111 '-j', '--maxjobs', action='store', type=int,
3112 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003113
3114 auth.add_auth_options(parser)
3115 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003116 if args:
3117 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003118 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003119
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003120 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003121 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003122 if options.field.startswith('desc'):
3123 print cl.GetDescription()
3124 elif options.field == 'id':
3125 issueid = cl.GetIssue()
3126 if issueid:
3127 print issueid
3128 elif options.field == 'patch':
3129 patchset = cl.GetPatchset()
3130 if patchset:
3131 print patchset
3132 elif options.field == 'url':
3133 url = cl.GetIssueURL()
3134 if url:
3135 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003136 return 0
3137
3138 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3139 if not branches:
3140 print('No local branch found.')
3141 return 0
3142
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003143 changes = (
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003144 Changelist(branchref=b, auth_config=auth_config)
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003145 for b in branches.splitlines())
3146 # TODO(tandrii): refactor to use CLs list instead of branches list.
3147 branches = [c.GetBranch() for c in changes]
3148 alignment = max(5, max(len(b) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003149 print 'Branches associated with reviews:'
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003150 output = get_cl_statuses(branches,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003151 fine_grained=not options.fast,
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003152 max_processes=options.maxjobs,
3153 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003154
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003155 branch_statuses = {}
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003156 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
3157 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003158 while branch not in branch_statuses:
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003159 b, i, status = output.next()
3160 branch_statuses[b] = (i, status)
3161 issue_url, status = branch_statuses.pop(branch)
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003162 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003163 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003164 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003165 color = ''
3166 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003167 status_str = '(%s)' % status if status else ''
3168 print ' %*s : %s%s %s%s' % (
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003169 alignment, ShortBranchName(branch), color, issue_url, status_str,
3170 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003171
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003172 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003173 print
3174 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003175 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003176 if not cl.GetIssue():
3177 print 'No issue assigned.'
3178 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003179 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00003180 if not options.fast:
3181 print 'Issue description:'
3182 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003183 return 0
3184
3185
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003186def colorize_CMDstatus_doc():
3187 """To be called once in main() to add colors to git cl status help."""
3188 colors = [i for i in dir(Fore) if i[0].isupper()]
3189
3190 def colorize_line(line):
3191 for color in colors:
3192 if color in line.upper():
3193 # Extract whitespaces first and the leading '-'.
3194 indent = len(line) - len(line.lstrip(' ')) + 1
3195 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3196 return line
3197
3198 lines = CMDstatus.__doc__.splitlines()
3199 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3200
3201
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003202@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003203def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003204 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003205
3206 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003207 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003208 parser.add_option('-r', '--reverse', action='store_true',
3209 help='Lookup the branch(es) for the specified issues. If '
3210 'no issues are specified, all branches with mapped '
3211 'issues will be listed.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003212 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003213 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003214 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003215
dnj@chromium.org406c4402015-03-03 17:22:28 +00003216 if options.reverse:
3217 branches = RunGit(['for-each-ref', 'refs/heads',
3218 '--format=%(refname:short)']).splitlines()
3219
3220 # Reverse issue lookup.
3221 issue_branch_map = {}
3222 for branch in branches:
3223 cl = Changelist(branchref=branch)
3224 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3225 if not args:
3226 args = sorted(issue_branch_map.iterkeys())
3227 for issue in args:
3228 if not issue:
3229 continue
3230 print 'Branch for issue number %s: %s' % (
3231 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
3232 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003233 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003234 if len(args) > 0:
3235 try:
3236 issue = int(args[0])
3237 except ValueError:
3238 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003239 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003240 cl.SetIssue(issue)
3241 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003242 return 0
3243
3244
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003245def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003246 """Shows or posts review comments for any changelist."""
3247 parser.add_option('-a', '--add-comment', dest='comment',
3248 help='comment to add to an issue')
3249 parser.add_option('-i', dest='issue',
3250 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003251 parser.add_option('-j', '--json-file',
3252 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003253 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003254 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003255 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003256
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003257 issue = None
3258 if options.issue:
3259 try:
3260 issue = int(options.issue)
3261 except ValueError:
3262 DieWithError('A review issue id is expected to be a number')
3263
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003264 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003265
3266 if options.comment:
3267 cl.AddComment(options.comment)
3268 return 0
3269
3270 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003271 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003272 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003273 summary.append({
3274 'date': message['date'],
3275 'lgtm': False,
3276 'message': message['text'],
3277 'not_lgtm': False,
3278 'sender': message['sender'],
3279 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003280 if message['disapproval']:
3281 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003282 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003283 elif message['approval']:
3284 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003285 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003286 elif message['sender'] == data['owner_email']:
3287 color = Fore.MAGENTA
3288 else:
3289 color = Fore.BLUE
3290 print '\n%s%s %s%s' % (
3291 color, message['date'].split('.', 1)[0], message['sender'],
3292 Fore.RESET)
3293 if message['text'].strip():
3294 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00003295 if options.json_file:
3296 with open(options.json_file, 'wb') as f:
3297 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003298 return 0
3299
3300
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003301@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003302def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003303 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003304 parser.add_option('-d', '--display', action='store_true',
3305 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003306 parser.add_option('-n', '--new-description',
3307 help='New description to set for this issue (- for stdin)')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003308
3309 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003310 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003311 options, args = parser.parse_args(args)
3312 _process_codereview_select_options(parser, options)
3313
3314 target_issue = None
3315 if len(args) > 0:
3316 issue_arg = ParseIssueNumberArgument(args[0])
3317 if not issue_arg.valid:
3318 parser.print_help()
3319 return 1
3320 target_issue = issue_arg.issue
3321
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003322 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003323
3324 cl = Changelist(
3325 auth_config=auth_config, issue=target_issue,
3326 codereview=options.forced_codereview)
3327
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003328 if not cl.GetIssue():
3329 DieWithError('This branch has no associated changelist.')
3330 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003331
smut@google.com34fb6b12015-07-13 20:03:26 +00003332 if options.display:
tandrii@chromium.org8c3b4422016-04-27 13:11:18 +00003333 print description.description
smut@google.com34fb6b12015-07-13 20:03:26 +00003334 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003335
3336 if options.new_description:
3337 text = options.new_description
3338 if text == '-':
3339 text = '\n'.join(l.rstrip() for l in sys.stdin)
3340
3341 description.set_description(text)
3342 else:
3343 description.prompt()
3344
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003345 if cl.GetDescription() != description.description:
3346 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003347 return 0
3348
3349
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003350def CreateDescriptionFromLog(args):
3351 """Pulls out the commit log to use as a base for the CL description."""
3352 log_args = []
3353 if len(args) == 1 and not args[0].endswith('.'):
3354 log_args = [args[0] + '..']
3355 elif len(args) == 1 and args[0].endswith('...'):
3356 log_args = [args[0][:-1]]
3357 elif len(args) == 2:
3358 log_args = [args[0] + '..' + args[1]]
3359 else:
3360 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003361 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003362
3363
thestig@chromium.org44202a22014-03-11 19:22:18 +00003364def CMDlint(parser, args):
3365 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003366 parser.add_option('--filter', action='append', metavar='-x,+y',
3367 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003368 auth.add_auth_options(parser)
3369 options, args = parser.parse_args(args)
3370 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003371
3372 # Access to a protected member _XX of a client class
3373 # pylint: disable=W0212
3374 try:
3375 import cpplint
3376 import cpplint_chromium
3377 except ImportError:
3378 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
3379 return 1
3380
3381 # Change the current working directory before calling lint so that it
3382 # shows the correct base.
3383 previous_cwd = os.getcwd()
3384 os.chdir(settings.GetRoot())
3385 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003386 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003387 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3388 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003389 if not files:
3390 print "Cannot lint an empty CL"
3391 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003392
3393 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003394 command = args + files
3395 if options.filter:
3396 command = ['--filter=' + ','.join(options.filter)] + command
3397 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003398
3399 white_regex = re.compile(settings.GetLintRegex())
3400 black_regex = re.compile(settings.GetLintIgnoreRegex())
3401 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3402 for filename in filenames:
3403 if white_regex.match(filename):
3404 if black_regex.match(filename):
3405 print "Ignoring file %s" % filename
3406 else:
3407 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3408 extra_check_functions)
3409 else:
3410 print "Skipping file %s" % filename
3411 finally:
3412 os.chdir(previous_cwd)
3413 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
3414 if cpplint._cpplint_state.error_count != 0:
3415 return 1
3416 return 0
3417
3418
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003419def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003420 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003421 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003422 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003423 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003424 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003425 auth.add_auth_options(parser)
3426 options, args = parser.parse_args(args)
3427 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003428
sbc@chromium.org71437c02015-04-09 19:29:40 +00003429 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00003430 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003431 return 1
3432
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003433 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003434 if args:
3435 base_branch = args[0]
3436 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003437 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003438 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003439
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003440 cl.RunHook(
3441 committing=not options.upload,
3442 may_prompt=False,
3443 verbose=options.verbose,
3444 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003445 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003446
3447
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003448def GenerateGerritChangeId(message):
3449 """Returns Ixxxxxx...xxx change id.
3450
3451 Works the same way as
3452 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3453 but can be called on demand on all platforms.
3454
3455 The basic idea is to generate git hash of a state of the tree, original commit
3456 message, author/committer info and timestamps.
3457 """
3458 lines = []
3459 tree_hash = RunGitSilent(['write-tree'])
3460 lines.append('tree %s' % tree_hash.strip())
3461 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3462 if code == 0:
3463 lines.append('parent %s' % parent.strip())
3464 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3465 lines.append('author %s' % author.strip())
3466 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3467 lines.append('committer %s' % committer.strip())
3468 lines.append('')
3469 # Note: Gerrit's commit-hook actually cleans message of some lines and
3470 # whitespace. This code is not doing this, but it clearly won't decrease
3471 # entropy.
3472 lines.append(message)
3473 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3474 stdin='\n'.join(lines))
3475 return 'I%s' % change_hash.strip()
3476
3477
wittman@chromium.org455dc922015-01-26 20:15:50 +00003478def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3479 """Computes the remote branch ref to use for the CL.
3480
3481 Args:
3482 remote (str): The git remote for the CL.
3483 remote_branch (str): The git remote branch for the CL.
3484 target_branch (str): The target branch specified by the user.
3485 pending_prefix (str): The pending prefix from the settings.
3486 """
3487 if not (remote and remote_branch):
3488 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003489
wittman@chromium.org455dc922015-01-26 20:15:50 +00003490 if target_branch:
3491 # Cannonicalize branch references to the equivalent local full symbolic
3492 # refs, which are then translated into the remote full symbolic refs
3493 # below.
3494 if '/' not in target_branch:
3495 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3496 else:
3497 prefix_replacements = (
3498 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3499 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3500 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3501 )
3502 match = None
3503 for regex, replacement in prefix_replacements:
3504 match = re.search(regex, target_branch)
3505 if match:
3506 remote_branch = target_branch.replace(match.group(0), replacement)
3507 break
3508 if not match:
3509 # This is a branch path but not one we recognize; use as-is.
3510 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003511 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3512 # Handle the refs that need to land in different refs.
3513 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003514
wittman@chromium.org455dc922015-01-26 20:15:50 +00003515 # Create the true path to the remote branch.
3516 # Does the following translation:
3517 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3518 # * refs/remotes/origin/master -> refs/heads/master
3519 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3520 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3521 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3522 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3523 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3524 'refs/heads/')
3525 elif remote_branch.startswith('refs/remotes/branch-heads'):
3526 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3527 # If a pending prefix exists then replace refs/ with it.
3528 if pending_prefix:
3529 remote_branch = remote_branch.replace('refs/', pending_prefix)
3530 return remote_branch
3531
3532
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003533def cleanup_list(l):
3534 """Fixes a list so that comma separated items are put as individual items.
3535
3536 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3537 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3538 """
3539 items = sum((i.split(',') for i in l), [])
3540 stripped_items = (i.strip() for i in items)
3541 return sorted(filter(None, stripped_items))
3542
3543
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003544@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003545def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003546 """Uploads the current changelist to codereview.
3547
3548 Can skip dependency patchset uploads for a branch by running:
3549 git config branch.branch_name.skip-deps-uploads True
3550 To unset run:
3551 git config --unset branch.branch_name.skip-deps-uploads
3552 Can also set the above globally by using the --global flag.
3553 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003554 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3555 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003556 parser.add_option('--bypass-watchlists', action='store_true',
3557 dest='bypass_watchlists',
3558 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003559 parser.add_option('-f', action='store_true', dest='force',
3560 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003561 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003562 parser.add_option('-t', dest='title',
3563 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003564 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003565 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003566 help='reviewer email addresses')
3567 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003568 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003569 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003570 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003571 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003572 parser.add_option('--emulate_svn_auto_props',
3573 '--emulate-svn-auto-props',
3574 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003575 dest="emulate_svn_auto_props",
3576 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003577 parser.add_option('-c', '--use-commit-queue', action='store_true',
3578 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003579 parser.add_option('--private', action='store_true',
3580 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003581 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003582 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003583 metavar='TARGET',
3584 help='Apply CL to remote ref TARGET. ' +
3585 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003586 parser.add_option('--squash', action='store_true',
3587 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003588 parser.add_option('--no-squash', action='store_true',
3589 help='Don\'t squash multiple commits into one ' +
3590 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003591 parser.add_option('--email', default=None,
3592 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003593 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3594 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003595 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3596 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003597 help='Send the patchset to do a CQ dry run right after '
3598 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003599 parser.add_option('--dependencies', action='store_true',
3600 help='Uploads CLs of all the local branches that depend on '
3601 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003602
rmistry@google.com2dd99862015-06-22 12:22:18 +00003603 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003604 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003605 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003606 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003607 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003608 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003609 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003610
sbc@chromium.org71437c02015-04-09 19:29:40 +00003611 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003612 return 1
3613
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003614 options.reviewers = cleanup_list(options.reviewers)
3615 options.cc = cleanup_list(options.cc)
3616
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003617 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3618 settings.GetIsGerrit()
3619
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003620 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00003621 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003622
3623
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003624def IsSubmoduleMergeCommit(ref):
3625 # When submodules are added to the repo, we expect there to be a single
3626 # non-git-svn merge commit at remote HEAD with a signature comment.
3627 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003628 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003629 return RunGit(cmd) != ''
3630
3631
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003632def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003633 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003634
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003635 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3636 upstream and closes the issue automatically and atomically.
3637
3638 Otherwise (in case of Rietveld):
3639 Squashes branch into a single commit.
3640 Updates changelog with metadata (e.g. pointer to review).
3641 Pushes/dcommits the code upstream.
3642 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003643 """
3644 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3645 help='bypass upload presubmit hook')
3646 parser.add_option('-m', dest='message',
3647 help="override review description")
3648 parser.add_option('-f', action='store_true', dest='force',
3649 help="force yes to questions (don't prompt)")
3650 parser.add_option('-c', dest='contributor',
3651 help="external contributor for patch (appended to " +
3652 "description and used as author for git). Should be " +
3653 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003654 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003655 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003656 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003657 auth_config = auth.extract_auth_config_from_options(options)
3658
3659 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003660
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003661 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3662 if cl.IsGerrit():
3663 if options.message:
3664 # This could be implemented, but it requires sending a new patch to
3665 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3666 # Besides, Gerrit has the ability to change the commit message on submit
3667 # automatically, thus there is no need to support this option (so far?).
3668 parser.error('-m MESSAGE option is not supported for Gerrit.')
3669 if options.contributor:
3670 parser.error(
3671 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3672 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3673 'the contributor\'s "name <email>". If you can\'t upload such a '
3674 'commit for review, contact your repository admin and request'
3675 '"Forge-Author" permission.')
3676 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3677 options.verbose)
3678
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003679 current = cl.GetBranch()
3680 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3681 if not settings.GetIsGitSvn() and remote == '.':
3682 print
3683 print 'Attempting to push branch %r into another local branch!' % current
3684 print
3685 print 'Either reparent this branch on top of origin/master:'
3686 print ' git reparent-branch --root'
3687 print
3688 print 'OR run `git rebase-update` if you think the parent branch is already'
3689 print 'committed.'
3690 print
3691 print ' Current parent: %r' % upstream_branch
3692 return 1
3693
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003694 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003695 # Default to merging against our best guess of the upstream branch.
3696 args = [cl.GetUpstreamBranch()]
3697
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003698 if options.contributor:
3699 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
3700 print "Please provide contibutor as 'First Last <email@example.com>'"
3701 return 1
3702
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003703 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003704 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003705
sbc@chromium.org71437c02015-04-09 19:29:40 +00003706 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003707 return 1
3708
3709 # This rev-list syntax means "show all commits not in my branch that
3710 # are in base_branch".
3711 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3712 base_branch]).splitlines()
3713 if upstream_commits:
3714 print ('Base branch "%s" has %d commits '
3715 'not in this branch.' % (base_branch, len(upstream_commits)))
3716 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
3717 return 1
3718
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003719 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003720 svn_head = None
3721 if cmd == 'dcommit' or base_has_submodules:
3722 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
3723 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003724
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003725 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003726 # If the base_head is a submodule merge commit, the first parent of the
3727 # base_head should be a git-svn commit, which is what we're interested in.
3728 base_svn_head = base_branch
3729 if base_has_submodules:
3730 base_svn_head += '^1'
3731
3732 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003733 if extra_commits:
3734 print ('This branch has %d additional commits not upstreamed yet.'
3735 % len(extra_commits.splitlines()))
3736 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
3737 'before attempting to %s.' % (base_branch, cmd))
3738 return 1
3739
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003740 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003741 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003742 author = None
3743 if options.contributor:
3744 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003745 hook_results = cl.RunHook(
3746 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003747 may_prompt=not options.force,
3748 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003749 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003750 if not hook_results.should_continue():
3751 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003752
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003753 # Check the tree status if the tree status URL is set.
3754 status = GetTreeStatus()
3755 if 'closed' == status:
3756 print('The tree is closed. Please wait for it to reopen. Use '
3757 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3758 return 1
3759 elif 'unknown' == status:
3760 print('Unable to determine tree status. Please verify manually and '
3761 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3762 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003763
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003764 change_desc = ChangeDescription(options.message)
3765 if not change_desc.description and cl.GetIssue():
3766 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003767
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003768 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00003769 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003770 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00003771 else:
3772 print 'No description set.'
3773 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
3774 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003775
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003776 # Keep a separate copy for the commit message, because the commit message
3777 # contains the link to the Rietveld issue, while the Rietveld message contains
3778 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003779 # Keep a separate copy for the commit message.
3780 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00003781 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003782
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003783 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00003784 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00003785 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00003786 # after it. Add a period on a new line to circumvent this. Also add a space
3787 # before the period to make sure that Gitiles continues to correctly resolve
3788 # the URL.
3789 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003790 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003791 commit_desc.append_footer('Patch from %s.' % options.contributor)
3792
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00003793 print('Description:')
3794 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003795
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003796 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003797 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00003798 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003799
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003800 # We want to squash all this branch's commits into one commit with the proper
3801 # description. We do this by doing a "reset --soft" to the base branch (which
3802 # keeps the working copy the same), then dcommitting that. If origin/master
3803 # has a submodule merge commit, we'll also need to cherry-pick the squashed
3804 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003805 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003806 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
3807 # Delete the branches if they exist.
3808 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
3809 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
3810 result = RunGitWithCode(showref_cmd)
3811 if result[0] == 0:
3812 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003813
3814 # We might be in a directory that's present in this branch but not in the
3815 # trunk. Move up to the top of the tree so that git commands that expect a
3816 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003817 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003818 if rel_base_path:
3819 os.chdir(rel_base_path)
3820
3821 # Stuff our change into the merge branch.
3822 # We wrap in a try...finally block so if anything goes wrong,
3823 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003824 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003825 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003826 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003827 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003828 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00003829 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003830 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003831 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003832 RunGit(
3833 [
3834 'commit', '--author', options.contributor,
3835 '-m', commit_desc.description,
3836 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003837 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003838 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003839 if base_has_submodules:
3840 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
3841 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
3842 RunGit(['checkout', CHERRY_PICK_BRANCH])
3843 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003844 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003845 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003846 mirror = settings.GetGitMirror(remote)
3847 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003848 pending_prefix = settings.GetPendingRefPrefix()
3849 if not pending_prefix or branch.startswith(pending_prefix):
3850 # If not using refs/pending/heads/* at all, or target ref is already set
3851 # to pending, then push to the target ref directly.
3852 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003853 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003854 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003855 else:
3856 # Cherry-pick the change on top of pending ref and then push it.
3857 assert branch.startswith('refs/'), branch
3858 assert pending_prefix[-1] == '/', pending_prefix
3859 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003860 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003861 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003862 if retcode == 0:
3863 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003864 else:
3865 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003866 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003867 'svn', 'dcommit',
3868 '-C%s' % options.similarity,
3869 '--no-rebase', '--rmdir',
3870 ]
3871 if settings.GetForceHttpsCommitUrl():
3872 # Allow forcing https commit URLs for some projects that don't allow
3873 # committing to http URLs (like Google Code).
3874 remote_url = cl.GetGitSvnRemoteUrl()
3875 if urlparse.urlparse(remote_url).scheme == 'http':
3876 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003877 cmd_args.append('--commit-url=%s' % remote_url)
3878 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003879 if 'Committed r' in output:
3880 revision = re.match(
3881 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
3882 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003883 finally:
3884 # And then swap back to the original branch and clean up.
3885 RunGit(['checkout', '-q', cl.GetBranch()])
3886 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003887 if base_has_submodules:
3888 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003889
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003890 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003891 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003892 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003893
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003894 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003895 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003896 try:
3897 revision = WaitForRealCommit(remote, revision, base_branch, branch)
3898 # We set pushed_to_pending to False, since it made it all the way to the
3899 # real ref.
3900 pushed_to_pending = False
3901 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003902 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003903
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003904 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003905 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003906 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003907 if not to_pending:
3908 if viewvc_url and revision:
3909 change_desc.append_footer(
3910 'Committed: %s%s' % (viewvc_url, revision))
3911 elif revision:
3912 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003913 print ('Closing issue '
3914 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003915 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003916 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003917 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00003918 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00003919 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00003920 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003921 if options.bypass_hooks:
3922 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
3923 else:
3924 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00003925 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003926 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003927
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003928 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003929 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
3930 print 'The commit is in the pending queue (%s).' % pending_ref
3931 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00003932 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003933 'footer.' % branch)
3934
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003935 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
3936 if os.path.isfile(hook):
3937 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003938
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003939 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003940
3941
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003942def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
3943 print
3944 print 'Waiting for commit to be landed on %s...' % real_ref
3945 print '(If you are impatient, you may Ctrl-C once without harm)'
3946 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
3947 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003948 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003949
3950 loop = 0
3951 while True:
3952 sys.stdout.write('fetching (%d)... \r' % loop)
3953 sys.stdout.flush()
3954 loop += 1
3955
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003956 if mirror:
3957 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003958 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
3959 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
3960 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
3961 for commit in commits.splitlines():
3962 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
3963 print 'Found commit on %s' % real_ref
3964 return commit
3965
3966 current_rev = to_rev
3967
3968
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003969def PushToGitPending(remote, pending_ref, upstream_ref):
3970 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
3971
3972 Returns:
3973 (retcode of last operation, output log of last operation).
3974 """
3975 assert pending_ref.startswith('refs/'), pending_ref
3976 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
3977 cherry = RunGit(['rev-parse', 'HEAD']).strip()
3978 code = 0
3979 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003980 max_attempts = 3
3981 attempts_left = max_attempts
3982 while attempts_left:
3983 if attempts_left != max_attempts:
3984 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
3985 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003986
3987 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003988 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003989 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003990 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003991 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003992 print 'Fetch failed with exit code %d.' % code
3993 if out.strip():
3994 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003995 continue
3996
3997 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003998 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003999 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004000 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004001 if code:
4002 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004003 'Your patch doesn\'t apply cleanly to ref \'%s\', '
4004 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004005 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
4006 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004007 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004008 return code, out
4009
4010 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004011 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004012 code, out = RunGitWithCode(
4013 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4014 if code == 0:
4015 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004016 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004017 return code, out
4018
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004019 print 'Push failed with exit code %d.' % code
4020 if out.strip():
4021 print out.strip()
4022 if IsFatalPushFailure(out):
4023 print (
4024 'Fatal push error. Make sure your .netrc credentials and git '
4025 'user.email are correct and you have push access to the repo.')
4026 return code, out
4027
4028 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004029 return code, out
4030
4031
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004032def IsFatalPushFailure(push_stdout):
4033 """True if retrying push won't help."""
4034 return '(prohibited by Gerrit)' in push_stdout
4035
4036
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004037@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004038def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004039 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004040 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004041 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004042 # If it looks like previous commits were mirrored with git-svn.
4043 message = """This repository appears to be a git-svn mirror, but no
4044upstream SVN master is set. You probably need to run 'git auto-svn' once."""
4045 else:
4046 message = """This doesn't appear to be an SVN repository.
4047If your project has a true, writeable git repository, you probably want to run
4048'git cl land' instead.
4049If your project has a git mirror of an upstream SVN master, you probably need
4050to run 'git svn init'.
4051
4052Using the wrong command might cause your commit to appear to succeed, and the
4053review to be closed, without actually landing upstream. If you choose to
4054proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004055 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004056 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004057 return SendUpstream(parser, args, 'dcommit')
4058
4059
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004060@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004061def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004062 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004063 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004064 print('This appears to be an SVN repository.')
4065 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004066 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004067 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004068 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004069
4070
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004071@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004072def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004073 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004074 parser.add_option('-b', dest='newbranch',
4075 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004076 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004077 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004078 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4079 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004080 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004081 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004082 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004083 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004084 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004085 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004086
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004087
4088 group = optparse.OptionGroup(
4089 parser,
4090 'Options for continuing work on the current issue uploaded from a '
4091 'different clone (e.g. different machine). Must be used independently '
4092 'from the other options. No issue number should be specified, and the '
4093 'branch must have an issue number associated with it')
4094 group.add_option('--reapply', action='store_true', dest='reapply',
4095 help='Reset the branch and reapply the issue.\n'
4096 'CAUTION: This will undo any local changes in this '
4097 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004098
4099 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004100 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004101 parser.add_option_group(group)
4102
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004103 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004104 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004105 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004106 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004107 auth_config = auth.extract_auth_config_from_options(options)
4108
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004109 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004110
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004111 issue_arg = None
4112 if options.reapply :
4113 if len(args) > 0:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004114 parser.error('--reapply implies no additional arguments.')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004115
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004116 issue_arg = cl.GetIssue()
4117 upstream = cl.GetUpstreamBranch()
4118 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004119 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004120
4121 RunGit(['reset', '--hard', upstream])
4122 if options.pull:
4123 RunGit(['pull'])
4124 else:
4125 if len(args) != 1:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004126 parser.error('Must specify issue number or url')
4127 issue_arg = args[0]
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004128
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004129 if not issue_arg:
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004130 parser.print_help()
4131 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004132
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004133 if cl.IsGerrit():
4134 if options.reject:
4135 parser.error('--reject is not supported with Gerrit codereview.')
4136 if options.nocommit:
4137 parser.error('--nocommit is not supported with Gerrit codereview.')
4138 if options.directory:
4139 parser.error('--directory is not supported with Gerrit codereview.')
4140
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004141 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004142 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004143 return 1
4144
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004145 if options.newbranch:
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004146 if options.reapply:
4147 parser.error("--reapply excludes any option other than --pull")
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004148 if options.force:
4149 RunGit(['branch', '-D', options.newbranch],
4150 stderr=subprocess2.PIPE, error_ok=True)
4151 RunGit(['checkout', '-b', options.newbranch,
4152 Changelist().GetUpstreamBranch()])
4153
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004154 return cl.CMDPatchIssue(issue_arg, options.reject, options.nocommit,
4155 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004156
4157
4158def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004159 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004160 # Provide a wrapper for git svn rebase to help avoid accidental
4161 # git svn dcommit.
4162 # It's the only command that doesn't use parser at all since we just defer
4163 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004164
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004165 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004166
4167
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004168def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004169 """Fetches the tree status and returns either 'open', 'closed',
4170 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004171 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004172 if url:
4173 status = urllib2.urlopen(url).read().lower()
4174 if status.find('closed') != -1 or status == '0':
4175 return 'closed'
4176 elif status.find('open') != -1 or status == '1':
4177 return 'open'
4178 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004179 return 'unset'
4180
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004181
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004182def GetTreeStatusReason():
4183 """Fetches the tree status from a json url and returns the message
4184 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004185 url = settings.GetTreeStatusUrl()
4186 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004187 connection = urllib2.urlopen(json_url)
4188 status = json.loads(connection.read())
4189 connection.close()
4190 return status['message']
4191
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004192
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004193def GetBuilderMaster(bot_list):
4194 """For a given builder, fetch the master from AE if available."""
4195 map_url = 'https://builders-map.appspot.com/'
4196 try:
4197 master_map = json.load(urllib2.urlopen(map_url))
4198 except urllib2.URLError as e:
4199 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4200 (map_url, e))
4201 except ValueError as e:
4202 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4203 if not master_map:
4204 return None, 'Failed to build master map.'
4205
4206 result_master = ''
4207 for bot in bot_list:
4208 builder = bot.split(':', 1)[0]
4209 master_list = master_map.get(builder, [])
4210 if not master_list:
4211 return None, ('No matching master for builder %s.' % builder)
4212 elif len(master_list) > 1:
4213 return None, ('The builder name %s exists in multiple masters %s.' %
4214 (builder, master_list))
4215 else:
4216 cur_master = master_list[0]
4217 if not result_master:
4218 result_master = cur_master
4219 elif result_master != cur_master:
4220 return None, 'The builders do not belong to the same master.'
4221 return result_master, None
4222
4223
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004224def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004225 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004226 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004227 status = GetTreeStatus()
4228 if 'unset' == status:
4229 print 'You must configure your tree status URL by running "git cl config".'
4230 return 2
4231
4232 print "The tree is %s" % status
4233 print
4234 print GetTreeStatusReason()
4235 if status != 'open':
4236 return 1
4237 return 0
4238
4239
maruel@chromium.org15192402012-09-06 12:38:29 +00004240def CMDtry(parser, args):
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004241 """Triggers try jobs through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00004242 group = optparse.OptionGroup(parser, "Try job options")
4243 group.add_option(
4244 "-b", "--bot", action="append",
4245 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
4246 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004247 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00004248 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004249 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00004250 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004251 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00004252 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004253 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004254 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004255 "-r", "--revision",
4256 help="Revision to use for the try job; default: the "
4257 "revision will be determined by the try server; see "
4258 "its waterfall for more info")
4259 group.add_option(
4260 "-c", "--clobber", action="store_true", default=False,
4261 help="Force a clobber before building; e.g. don't do an "
4262 "incremental build")
4263 group.add_option(
4264 "--project",
4265 help="Override which project to use. Projects are defined "
4266 "server-side to define what default bot set to use")
4267 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004268 "-p", "--property", dest="properties", action="append", default=[],
4269 help="Specify generic properties in the form -p key1=value1 -p "
4270 "key2=value2 etc (buildbucket only). The value will be treated as "
4271 "json if decodable, or as string otherwise.")
4272 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004273 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004274 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004275 "--use-rietveld", action="store_true", default=False,
4276 help="Use Rietveld to trigger try jobs.")
4277 group.add_option(
4278 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4279 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004280 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004281 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004282 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004283 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004284
machenbach@chromium.org45453142015-09-15 08:45:22 +00004285 if options.use_rietveld and options.properties:
4286 parser.error('Properties can only be specified with buildbucket')
4287
4288 # Make sure that all properties are prop=value pairs.
4289 bad_params = [x for x in options.properties if '=' not in x]
4290 if bad_params:
4291 parser.error('Got properties with missing "=": %s' % bad_params)
4292
maruel@chromium.org15192402012-09-06 12:38:29 +00004293 if args:
4294 parser.error('Unknown arguments: %s' % args)
4295
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004296 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004297 if not cl.GetIssue():
4298 parser.error('Need to upload first')
4299
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004300 if cl.IsGerrit():
4301 parser.error(
4302 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4303 'If your project has Commit Queue, dry run is a workaround:\n'
4304 ' git cl set-commit --dry-run')
4305 # Code below assumes Rietveld issue.
4306 # TODO(tandrii): actually implement for Gerrit http://crbug.com/599931.
4307
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004308 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004309 if props.get('closed'):
4310 parser.error('Cannot send tryjobs for a closed CL')
4311
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004312 if props.get('private'):
4313 parser.error('Cannot use trybots with private issue')
4314
maruel@chromium.org15192402012-09-06 12:38:29 +00004315 if not options.name:
4316 options.name = cl.GetBranch()
4317
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004318 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004319 options.master, err_msg = GetBuilderMaster(options.bot)
4320 if err_msg:
4321 parser.error('Tryserver master cannot be found because: %s\n'
4322 'Please manually specify the tryserver master'
4323 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004324
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004325 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004326 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004327 if not options.bot:
4328 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004329
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004330 # Get try masters from PRESUBMIT.py files.
4331 masters = presubmit_support.DoGetTryMasters(
4332 change,
4333 change.LocalPaths(),
4334 settings.GetRoot(),
4335 None,
4336 None,
4337 options.verbose,
4338 sys.stdout)
4339 if masters:
4340 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004341
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004342 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4343 options.bot = presubmit_support.DoGetTrySlaves(
4344 change,
4345 change.LocalPaths(),
4346 settings.GetRoot(),
4347 None,
4348 None,
4349 options.verbose,
4350 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004351
4352 if not options.bot:
4353 # Get try masters from cq.cfg if any.
4354 # TODO(tandrii): some (but very few) projects store cq.cfg in different
4355 # location.
4356 cq_cfg = os.path.join(change.RepositoryRoot(),
4357 'infra', 'config', 'cq.cfg')
4358 if os.path.exists(cq_cfg):
4359 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00004360 cq_masters = commit_queue.get_master_builder_map(
4361 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004362 for master, builders in cq_masters.iteritems():
4363 for builder in builders:
4364 # Skip presubmit builders, because these will fail without LGTM.
machenbach@chromium.org2403e802016-04-29 12:34:42 +00004365 masters.setdefault(master, {})[builder] = ['defaulttests']
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004366 if masters:
4367 return masters
4368
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004369 if not options.bot:
4370 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00004371
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004372 builders_and_tests = {}
4373 # TODO(machenbach): The old style command-line options don't support
4374 # multiple try masters yet.
4375 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4376 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4377
4378 for bot in old_style:
4379 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004380 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004381 elif ',' in bot:
4382 parser.error('Specify one bot per --bot flag')
4383 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004384 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004385
4386 for bot, tests in new_style:
4387 builders_and_tests.setdefault(bot, []).extend(tests)
4388
4389 # Return a master map with one master to be backwards compatible. The
4390 # master name defaults to an empty string, which will cause the master
4391 # not to be set on rietveld (deprecated).
4392 return {options.master: builders_and_tests}
4393
4394 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004395
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004396 for builders in masters.itervalues():
4397 if any('triggered' in b for b in builders):
4398 print >> sys.stderr, (
4399 'ERROR You are trying to send a job to a triggered bot. This type of'
4400 ' bot requires an\ninitial job from a parent (usually a builder). '
4401 'Instead send your job to the parent.\n'
4402 'Bot list: %s' % builders)
4403 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004404
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004405 patchset = cl.GetMostRecentPatchset()
4406 if patchset and patchset != cl.GetPatchset():
4407 print(
4408 '\nWARNING Mismatch between local config and server. Did a previous '
4409 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4410 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004411 if options.luci:
4412 trigger_luci_job(cl, masters, options)
4413 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004414 try:
4415 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4416 except BuildbucketResponseException as ex:
4417 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004418 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004419 except Exception as e:
4420 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4421 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
4422 e, stacktrace)
4423 return 1
4424 else:
4425 try:
4426 cl.RpcServer().trigger_distributed_try_jobs(
4427 cl.GetIssue(), patchset, options.name, options.clobber,
4428 options.revision, masters)
4429 except urllib2.HTTPError as e:
4430 if e.code == 404:
4431 print('404 from rietveld; '
4432 'did you mean to use "git try" instead of "git cl try"?')
4433 return 1
4434 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004435
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004436 for (master, builders) in sorted(masters.iteritems()):
4437 if master:
4438 print 'Master: %s' % master
4439 length = max(len(builder) for builder in builders)
4440 for builder in sorted(builders):
4441 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00004442 return 0
4443
4444
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004445def CMDtry_results(parser, args):
4446 group = optparse.OptionGroup(parser, "Try job results options")
4447 group.add_option(
4448 "-p", "--patchset", type=int, help="patchset number if not current.")
4449 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004450 "--print-master", action='store_true', help="print master name as well.")
4451 group.add_option(
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004452 "--color", action='store_true', default=setup_color.IS_TTY,
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004453 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004454 group.add_option(
4455 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4456 help="Host of buildbucket. The default host is %default.")
4457 parser.add_option_group(group)
4458 auth.add_auth_options(parser)
4459 options, args = parser.parse_args(args)
4460 if args:
4461 parser.error('Unrecognized args: %s' % ' '.join(args))
4462
4463 auth_config = auth.extract_auth_config_from_options(options)
4464 cl = Changelist(auth_config=auth_config)
4465 if not cl.GetIssue():
4466 parser.error('Need to upload first')
4467
4468 if not options.patchset:
4469 options.patchset = cl.GetMostRecentPatchset()
4470 if options.patchset and options.patchset != cl.GetPatchset():
4471 print(
4472 '\nWARNING Mismatch between local config and server. Did a previous '
4473 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4474 'Continuing using\npatchset %s.\n' % options.patchset)
4475 try:
4476 jobs = fetch_try_jobs(auth_config, cl, options)
4477 except BuildbucketResponseException as ex:
4478 print 'Buildbucket error: %s' % ex
4479 return 1
4480 except Exception as e:
4481 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4482 print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
4483 e, stacktrace)
4484 return 1
4485 print_tryjobs(options, jobs)
4486 return 0
4487
4488
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004489@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004490def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004491 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004492 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004493 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004494 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004495
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004496 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004497 if args:
4498 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004499 branch = cl.GetBranch()
4500 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004501 cl = Changelist()
4502 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004503
4504 # Clear configured merge-base, if there is one.
4505 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004506 else:
4507 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004508 return 0
4509
4510
thestig@chromium.org00858c82013-12-02 23:08:03 +00004511def CMDweb(parser, args):
4512 """Opens the current CL in the web browser."""
4513 _, args = parser.parse_args(args)
4514 if args:
4515 parser.error('Unrecognized args: %s' % ' '.join(args))
4516
4517 issue_url = Changelist().GetIssueURL()
4518 if not issue_url:
4519 print >> sys.stderr, 'ERROR No issue to open'
4520 return 1
4521
4522 webbrowser.open(issue_url)
4523 return 0
4524
4525
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004526def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004527 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004528 parser.add_option('-d', '--dry-run', action='store_true',
4529 help='trigger in dry run mode')
4530 parser.add_option('-c', '--clear', action='store_true',
4531 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004532 auth.add_auth_options(parser)
4533 options, args = parser.parse_args(args)
4534 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004535 if args:
4536 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004537 if options.dry_run and options.clear:
4538 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4539
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004540 cl = Changelist(auth_config=auth_config)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004541 if options.clear:
4542 state = _CQState.CLEAR
4543 elif options.dry_run:
4544 state = _CQState.DRY_RUN
4545 else:
4546 state = _CQState.COMMIT
4547 if not cl.GetIssue():
4548 parser.error('Must upload the issue first')
4549 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004550 return 0
4551
4552
groby@chromium.org411034a2013-02-26 15:12:01 +00004553def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004554 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004555 auth.add_auth_options(parser)
4556 options, args = parser.parse_args(args)
4557 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004558 if args:
4559 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004560 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00004561 # Ensure there actually is an issue to close.
4562 cl.GetDescription()
4563 cl.CloseIssue()
4564 return 0
4565
4566
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004567def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004568 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004569 auth.add_auth_options(parser)
4570 options, args = parser.parse_args(args)
4571 auth_config = auth.extract_auth_config_from_options(options)
4572 if args:
4573 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004574
4575 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004576 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004577 # Staged changes would be committed along with the patch from last
4578 # upload, hence counted toward the "last upload" side in the final
4579 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004580 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004581 return 1
4582
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004583 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004584 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004585 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004586 if not issue:
4587 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004588 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004589 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004590
4591 # Create a new branch based on the merge-base
4592 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004593 # Clear cached branch in cl object, to avoid overwriting original CL branch
4594 # properties.
4595 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004596 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004597 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004598 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004599 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004600 return rtn
4601
wychen@chromium.org06928532015-02-03 02:11:29 +00004602 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004603 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00004604 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004605 finally:
4606 RunGit(['checkout', '-q', branch])
4607 RunGit(['branch', '-D', TMP_BRANCH])
4608
4609 return 0
4610
4611
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004612def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004613 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004614 parser.add_option(
4615 '--no-color',
4616 action='store_true',
4617 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004618 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004619 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004620 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004621
4622 author = RunGit(['config', 'user.email']).strip() or None
4623
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004624 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004625
4626 if args:
4627 if len(args) > 1:
4628 parser.error('Unknown args')
4629 base_branch = args[0]
4630 else:
4631 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004632 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004633
4634 change = cl.GetChange(base_branch, None)
4635 return owners_finder.OwnersFinder(
4636 [f.LocalPath() for f in
4637 cl.GetChange(base_branch, None).AffectedFiles()],
4638 change.RepositoryRoot(), author,
4639 fopen=file, os_path=os.path, glob=glob.glob,
4640 disable_color=options.no_color).run()
4641
4642
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004643def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004644 """Generates a diff command."""
4645 # Generate diff for the current branch's changes.
4646 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4647 upstream_commit, '--' ]
4648
4649 if args:
4650 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004651 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004652 diff_cmd.append(arg)
4653 else:
4654 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004655
4656 return diff_cmd
4657
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004658def MatchingFileType(file_name, extensions):
4659 """Returns true if the file name ends with one of the given extensions."""
4660 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004661
enne@chromium.org555cfe42014-01-29 18:21:39 +00004662@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004663def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004664 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00004665 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004666 GN_EXTS = ['.gn', '.gni']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004667 parser.add_option('--full', action='store_true',
4668 help='Reformat the full content of all touched files')
4669 parser.add_option('--dry-run', action='store_true',
4670 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004671 parser.add_option('--python', action='store_true',
4672 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004673 parser.add_option('--diff', action='store_true',
4674 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004675 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004676
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004677 # git diff generates paths against the root of the repository. Change
4678 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004679 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004680 if rel_base_path:
4681 os.chdir(rel_base_path)
4682
digit@chromium.org29e47272013-05-17 17:01:46 +00004683 # Grab the merge-base commit, i.e. the upstream commit of the current
4684 # branch when it was created or the last time it was rebased. This is
4685 # to cover the case where the user may have called "git fetch origin",
4686 # moving the origin branch to a newer commit, but hasn't rebased yet.
4687 upstream_commit = None
4688 cl = Changelist()
4689 upstream_branch = cl.GetUpstreamBranch()
4690 if upstream_branch:
4691 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4692 upstream_commit = upstream_commit.strip()
4693
4694 if not upstream_commit:
4695 DieWithError('Could not find base commit for this branch. '
4696 'Are you in detached state?')
4697
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004698 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4699 diff_output = RunGit(changed_files_cmd)
4700 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004701 # Filter out files deleted by this CL
4702 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004703
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004704 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4705 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4706 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004707 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004708
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004709 top_dir = os.path.normpath(
4710 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
4711
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004712 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4713 # formatted. This is used to block during the presubmit.
4714 return_value = 0
4715
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004716 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004717 # Locate the clang-format binary in the checkout
4718 try:
4719 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
4720 except clang_format.NotFoundError, e:
4721 DieWithError(e)
4722
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004723 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004724 cmd = [clang_format_tool]
4725 if not opts.dry_run and not opts.diff:
4726 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004727 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004728 if opts.diff:
4729 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004730 else:
4731 env = os.environ.copy()
4732 env['PATH'] = str(os.path.dirname(clang_format_tool))
4733 try:
4734 script = clang_format.FindClangFormatScriptInChromiumTree(
4735 'clang-format-diff.py')
4736 except clang_format.NotFoundError, e:
4737 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004738
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004739 cmd = [sys.executable, script, '-p0']
4740 if not opts.dry_run and not opts.diff:
4741 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004742
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004743 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
4744 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004745
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004746 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
4747 if opts.diff:
4748 sys.stdout.write(stdout)
4749 if opts.dry_run and len(stdout) > 0:
4750 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004751
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004752 # Similar code to above, but using yapf on .py files rather than clang-format
4753 # on C/C++ files
4754 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004755 yapf_tool = gclient_utils.FindExecutable('yapf')
4756 if yapf_tool is None:
4757 DieWithError('yapf not found in PATH')
4758
4759 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004760 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004761 cmd = [yapf_tool]
4762 if not opts.dry_run and not opts.diff:
4763 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004764 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004765 if opts.diff:
4766 sys.stdout.write(stdout)
4767 else:
4768 # TODO(sbc): yapf --lines mode still has some issues.
4769 # https://github.com/google/yapf/issues/154
4770 DieWithError('--python currently only works with --full')
4771
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004772 # Dart's formatter does not have the nice property of only operating on
4773 # modified chunks, so hard code full.
4774 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004775 try:
4776 command = [dart_format.FindDartFmtToolInChromiumTree()]
4777 if not opts.dry_run and not opts.diff:
4778 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004779 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004780
ppi@chromium.org6593d932016-03-03 15:41:15 +00004781 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004782 if opts.dry_run and stdout:
4783 return_value = 2
4784 except dart_format.NotFoundError as e:
erikcorry@chromium.org3e445022015-12-17 09:07:26 +00004785 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
4786 'found in this checkout. Files in other languages are still ' +
4787 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004788
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004789 # Format GN build files. Always run on full build files for canonical form.
4790 if gn_diff_files:
4791 cmd = ['gn', 'format']
4792 if not opts.dry_run and not opts.diff:
4793 cmd.append('--in-place')
4794 for gn_diff_file in gn_diff_files:
bsep@chromium.org627d9002016-04-29 00:00:52 +00004795 stdout = RunCommand(cmd + [gn_diff_file],
4796 shell=sys.platform == 'win32',
4797 cwd=top_dir)
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004798 if opts.diff:
4799 sys.stdout.write(stdout)
4800
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004801 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004802
4803
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004804@subcommand.usage('<codereview url or issue id>')
4805def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004806 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004807 _, args = parser.parse_args(args)
4808
4809 if len(args) != 1:
4810 parser.print_help()
4811 return 1
4812
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004813 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00004814 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004815 parser.print_help()
4816 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00004817 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004818
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004819 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00004820 output = RunGit(['config', '--local', '--get-regexp',
4821 r'branch\..*\.%s' % issueprefix],
4822 error_ok=True)
4823 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004824 if issue == target_issue:
4825 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004826
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004827 branches = []
4828 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00004829 branches.extend(find_issues(cls.IssueSettingSuffix()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004830 if len(branches) == 0:
4831 print 'No branch found for issue %s.' % target_issue
4832 return 1
4833 if len(branches) == 1:
4834 RunGit(['checkout', branches[0]])
4835 else:
4836 print 'Multiple branches match issue %s:' % target_issue
4837 for i in range(len(branches)):
4838 print '%d: %s' % (i, branches[i])
4839 which = raw_input('Choose by index: ')
4840 try:
4841 RunGit(['checkout', branches[int(which)]])
4842 except (IndexError, ValueError):
4843 print 'Invalid selection, not checking out any branch.'
4844 return 1
4845
4846 return 0
4847
4848
maruel@chromium.org29404b52014-09-08 22:58:00 +00004849def CMDlol(parser, args):
4850 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00004851 print zlib.decompress(base64.b64decode(
4852 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
4853 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
4854 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
4855 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00004856 return 0
4857
4858
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004859class OptionParser(optparse.OptionParser):
4860 """Creates the option parse and add --verbose support."""
4861 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004862 optparse.OptionParser.__init__(
4863 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004864 self.add_option(
4865 '-v', '--verbose', action='count', default=0,
4866 help='Use 2 times for more debugging info')
4867
4868 def parse_args(self, args=None, values=None):
4869 options, args = optparse.OptionParser.parse_args(self, args, values)
4870 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
4871 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
4872 return options, args
4873
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004874
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004875def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00004876 if sys.hexversion < 0x02060000:
4877 print >> sys.stderr, (
4878 '\nYour python version %s is unsupported, please upgrade.\n' %
4879 sys.version.split(' ', 1)[0])
4880 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004881
maruel@chromium.orgddd59412011-11-30 14:20:38 +00004882 # Reload settings.
4883 global settings
4884 settings = Settings()
4885
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004886 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004887 dispatcher = subcommand.CommandDispatcher(__name__)
4888 try:
4889 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00004890 except auth.AuthenticationError as e:
4891 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004892 except urllib2.HTTPError, e:
4893 if e.code != 500:
4894 raise
4895 DieWithError(
4896 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
4897 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00004898 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004899
4900
4901if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004902 # These affect sys.stdout so do it outside of main() to simplify mocks in
4903 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00004904 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004905 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00004906 try:
4907 sys.exit(main(sys.argv[1:]))
4908 except KeyboardInterrupt:
4909 sys.stderr.write('interrupted\n')
4910 sys.exit(1)