blob: 7463a63ed95d4ae471f3fbf274bbf995423bd342 [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
vapiera7fbd5a2016-06-16 09:17:49 -070010from __future__ import print_function
11
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000012from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000013from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000014import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000015import collections
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000016import glob
sheyang@google.com6ebaf782015-05-12 19:17:54 +000017import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000018import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000020import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import optparse
22import os
23import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000024import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025import sys
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
tandrii7400cf02016-06-21 08:48:07 -070065__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000066
tandrii9d2c7a32016-06-22 03:42:45 -070067COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000068DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000069POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000070DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000071GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
rmistry@google.comc68112d2015-03-03 12:48:06 +000072REFS_THAT_ALIAS_TO_OTHER_REFS = {
73 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
74 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
75}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000076
thestig@chromium.org44202a22014-03-11 19:22:18 +000077# Valid extensions for files we want to lint.
78DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
79DEFAULT_LINT_IGNORE_REGEX = r"$^"
80
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000081# Shortcut since it quickly becomes redundant.
82Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000083
maruel@chromium.orgddd59412011-11-30 14:20:38 +000084# Initialized in main()
85settings = None
86
87
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000088def DieWithError(message):
vapiera7fbd5a2016-06-16 09:17:49 -070089 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000090 sys.exit(1)
91
92
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000093def GetNoGitPagerEnv():
94 env = os.environ.copy()
95 # 'cat' is a magical git string that disables pagers on all platforms.
96 env['GIT_PAGER'] = 'cat'
97 return env
98
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000099
bsep@chromium.org627d9002016-04-29 00:00:52 +0000100def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000101 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000102 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000103 except subprocess2.CalledProcessError as e:
104 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000105 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000106 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000107 'Command "%s" failed.\n%s' % (
108 ' '.join(args), error_message or e.stdout or ''))
109 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000110
111
112def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000113 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000114 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000115
116
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000117def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000118 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000119 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000120 if suppress_stderr:
121 stderr = subprocess2.VOID
122 else:
123 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000124 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000125 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000126 stdout=subprocess2.PIPE,
127 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000128 return code, out[0]
129 except ValueError:
130 # When the subprocess fails, it returns None. That triggers a ValueError
131 # when trying to unpack the return value into (out, code).
132 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000133
134
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000135def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000136 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000137 return RunGitWithCode(args, suppress_stderr=True)[1]
138
139
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000140def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000141 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000142 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000143 return (version.startswith(prefix) and
144 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000145
146
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000147def BranchExists(branch):
148 """Return True if specified branch exists."""
149 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
150 suppress_stderr=True)
151 return not code
152
153
maruel@chromium.org90541732011-04-01 17:54:18 +0000154def ask_for_data(prompt):
155 try:
156 return raw_input(prompt)
157 except KeyboardInterrupt:
158 # Hide the exception.
159 sys.exit(1)
160
161
iannucci@chromium.org79540052012-10-19 23:15:26 +0000162def git_set_branch_value(key, value):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000163 branch = GetCurrentBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000164 if not branch:
165 return
166
167 cmd = ['config']
168 if isinstance(value, int):
169 cmd.append('--int')
170 git_key = 'branch.%s.%s' % (branch, key)
171 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000172
173
174def git_get_branch_default(key, default):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000175 branch = GetCurrentBranch()
iannucci@chromium.org79540052012-10-19 23:15:26 +0000176 if branch:
177 git_key = 'branch.%s.%s' % (branch, key)
178 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
179 try:
180 return int(stdout.strip())
181 except ValueError:
182 pass
183 return default
184
185
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000186def add_git_similarity(parser):
187 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000188 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000189 help='Sets the percentage that a pair of files need to match in order to'
190 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000191 parser.add_option(
192 '--find-copies', action='store_true',
193 help='Allows git to look for copies.')
194 parser.add_option(
195 '--no-find-copies', action='store_false', dest='find_copies',
196 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000197
198 old_parser_args = parser.parse_args
199 def Parse(args):
200 options, args = old_parser_args(args)
201
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000202 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000203 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000204 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000205 print('Note: Saving similarity of %d%% in git config.'
206 % options.similarity)
207 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000208
iannucci@chromium.org79540052012-10-19 23:15:26 +0000209 options.similarity = max(0, min(options.similarity, 100))
210
211 if options.find_copies is None:
212 options.find_copies = bool(
213 git_get_branch_default('git-find-copies', True))
214 else:
215 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000216
217 print('Using %d%% similarity for rename/copy detection. '
218 'Override with --similarity.' % options.similarity)
219
220 return options, args
221 parser.parse_args = Parse
222
223
machenbach@chromium.org45453142015-09-15 08:45:22 +0000224def _get_properties_from_options(options):
225 properties = dict(x.split('=', 1) for x in options.properties)
226 for key, val in properties.iteritems():
227 try:
228 properties[key] = json.loads(val)
229 except ValueError:
230 pass # If a value couldn't be evaluated, treat it as a string.
231 return properties
232
233
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000234def _prefix_master(master):
235 """Convert user-specified master name to full master name.
236
237 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
238 name, while the developers always use shortened master name
239 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
240 function does the conversion for buildbucket migration.
241 """
242 prefix = 'master.'
243 if master.startswith(prefix):
244 return master
245 return '%s%s' % (prefix, master)
246
247
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000248def _buildbucket_retry(operation_name, http, *args, **kwargs):
249 """Retries requests to buildbucket service and returns parsed json content."""
250 try_count = 0
251 while True:
252 response, content = http.request(*args, **kwargs)
253 try:
254 content_json = json.loads(content)
255 except ValueError:
256 content_json = None
257
258 # Buildbucket could return an error even if status==200.
259 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000260 error = content_json.get('error')
261 if error.get('code') == 403:
262 raise BuildbucketResponseException(
263 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000264 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000265 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000266 raise BuildbucketResponseException(msg)
267
268 if response.status == 200:
269 if not content_json:
270 raise BuildbucketResponseException(
271 'Buildbucket returns invalid json content: %s.\n'
272 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
273 content)
274 return content_json
275 if response.status < 500 or try_count >= 2:
276 raise httplib2.HttpLib2Error(content)
277
278 # status >= 500 means transient failures.
279 logging.debug('Transient errors when %s. Will retry.', operation_name)
280 time.sleep(0.5 + 1.5*try_count)
281 try_count += 1
282 assert False, 'unreachable'
283
284
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000285def trigger_luci_job(changelist, masters, options):
286 """Send a job to run on LUCI."""
287 issue_props = changelist.GetIssueProperties()
288 issue = changelist.GetIssue()
289 patchset = changelist.GetMostRecentPatchset()
290 for builders_and_tests in sorted(masters.itervalues()):
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000291 # TODO(hinoka et al): add support for other properties.
292 # Currently, this completely ignores testfilter and other properties.
293 for builder in sorted(builders_and_tests):
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000294 luci_trigger.trigger(
295 builder, 'HEAD', issue, patchset, issue_props['project'])
296
297
machenbach@chromium.org45453142015-09-15 08:45:22 +0000298def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000299 rietveld_url = settings.GetDefaultServerUrl()
300 rietveld_host = urlparse.urlparse(rietveld_url).hostname
301 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
302 http = authenticator.authorize(httplib2.Http())
303 http.force_exception_to_status_code = True
304 issue_props = changelist.GetIssueProperties()
305 issue = changelist.GetIssue()
306 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000307 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000308
309 buildbucket_put_url = (
310 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000311 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000312 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
313 hostname=rietveld_host,
314 issue=issue,
315 patch=patchset)
316
317 batch_req_body = {'builds': []}
318 print_text = []
319 print_text.append('Tried jobs on:')
320 for master, builders_and_tests in sorted(masters.iteritems()):
321 print_text.append('Master: %s' % master)
322 bucket = _prefix_master(master)
323 for builder, tests in sorted(builders_and_tests.iteritems()):
324 print_text.append(' %s: %s' % (builder, tests))
325 parameters = {
326 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000327 'changes': [{
328 'author': {'email': issue_props['owner_email']},
329 'revision': options.revision,
330 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000331 'properties': {
332 'category': category,
333 'issue': issue,
334 'master': master,
335 'patch_project': issue_props['project'],
336 'patch_storage': 'rietveld',
337 'patchset': patchset,
338 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000339 'rietveld': rietveld_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000340 },
341 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000342 if 'presubmit' in builder.lower():
343 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000344 if tests:
345 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000346 if properties:
347 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000348 if options.clobber:
349 parameters['properties']['clobber'] = True
350 batch_req_body['builds'].append(
351 {
352 'bucket': bucket,
353 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000354 'client_operation_id': str(uuid.uuid4()),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000355 'tags': ['builder:%s' % builder,
356 'buildset:%s' % buildset,
357 'master:%s' % master,
358 'user_agent:git_cl_try']
359 }
360 )
361
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000362 _buildbucket_retry(
363 'triggering tryjobs',
364 http,
365 buildbucket_put_url,
366 'PUT',
367 body=json.dumps(batch_req_body),
368 headers={'Content-Type': 'application/json'}
369 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000370 print_text.append('To see results here, run: git cl try-results')
371 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700372 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000373
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000374
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000375def fetch_try_jobs(auth_config, changelist, options):
376 """Fetches tryjobs from buildbucket.
377
378 Returns a map from build id to build info as json dictionary.
379 """
380 rietveld_url = settings.GetDefaultServerUrl()
381 rietveld_host = urlparse.urlparse(rietveld_url).hostname
382 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
383 if authenticator.has_cached_credentials():
384 http = authenticator.authorize(httplib2.Http())
385 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700386 print('Warning: Some results might be missing because %s' %
387 # Get the message on how to login.
388 (auth.LoginRequiredError(rietveld_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000389 http = httplib2.Http()
390
391 http.force_exception_to_status_code = True
392
393 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
394 hostname=rietveld_host,
395 issue=changelist.GetIssue(),
396 patch=options.patchset)
397 params = {'tag': 'buildset:%s' % buildset}
398
399 builds = {}
400 while True:
401 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
402 hostname=options.buildbucket_host,
403 params=urllib.urlencode(params))
404 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
405 for build in content.get('builds', []):
406 builds[build['id']] = build
407 if 'next_cursor' in content:
408 params['start_cursor'] = content['next_cursor']
409 else:
410 break
411 return builds
412
413
414def print_tryjobs(options, builds):
415 """Prints nicely result of fetch_try_jobs."""
416 if not builds:
vapiera7fbd5a2016-06-16 09:17:49 -0700417 print('No tryjobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000418 return
419
420 # Make a copy, because we'll be modifying builds dictionary.
421 builds = builds.copy()
422 builder_names_cache = {}
423
424 def get_builder(b):
425 try:
426 return builder_names_cache[b['id']]
427 except KeyError:
428 try:
429 parameters = json.loads(b['parameters_json'])
430 name = parameters['builder_name']
431 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700432 print('WARNING: failed to get builder name for build %s: %s' % (
433 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000434 name = None
435 builder_names_cache[b['id']] = name
436 return name
437
438 def get_bucket(b):
439 bucket = b['bucket']
440 if bucket.startswith('master.'):
441 return bucket[len('master.'):]
442 return bucket
443
444 if options.print_master:
445 name_fmt = '%%-%ds %%-%ds' % (
446 max(len(str(get_bucket(b))) for b in builds.itervalues()),
447 max(len(str(get_builder(b))) for b in builds.itervalues()))
448 def get_name(b):
449 return name_fmt % (get_bucket(b), get_builder(b))
450 else:
451 name_fmt = '%%-%ds' % (
452 max(len(str(get_builder(b))) for b in builds.itervalues()))
453 def get_name(b):
454 return name_fmt % get_builder(b)
455
456 def sort_key(b):
457 return b['status'], b.get('result'), get_name(b), b.get('url')
458
459 def pop(title, f, color=None, **kwargs):
460 """Pop matching builds from `builds` dict and print them."""
461
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000462 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000463 colorize = str
464 else:
465 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
466
467 result = []
468 for b in builds.values():
469 if all(b.get(k) == v for k, v in kwargs.iteritems()):
470 builds.pop(b['id'])
471 result.append(b)
472 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700473 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000474 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700475 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000476
477 total = len(builds)
478 pop(status='COMPLETED', result='SUCCESS',
479 title='Successes:', color=Fore.GREEN,
480 f=lambda b: (get_name(b), b.get('url')))
481 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
482 title='Infra Failures:', color=Fore.MAGENTA,
483 f=lambda b: (get_name(b), b.get('url')))
484 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
485 title='Failures:', color=Fore.RED,
486 f=lambda b: (get_name(b), b.get('url')))
487 pop(status='COMPLETED', result='CANCELED',
488 title='Canceled:', color=Fore.MAGENTA,
489 f=lambda b: (get_name(b),))
490 pop(status='COMPLETED', result='FAILURE',
491 failure_reason='INVALID_BUILD_DEFINITION',
492 title='Wrong master/builder name:', color=Fore.MAGENTA,
493 f=lambda b: (get_name(b),))
494 pop(status='COMPLETED', result='FAILURE',
495 title='Other failures:',
496 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
497 pop(status='COMPLETED',
498 title='Other finished:',
499 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
500 pop(status='STARTED',
501 title='Started:', color=Fore.YELLOW,
502 f=lambda b: (get_name(b), b.get('url')))
503 pop(status='SCHEDULED',
504 title='Scheduled:',
505 f=lambda b: (get_name(b), 'id=%s' % b['id']))
506 # The last section is just in case buildbucket API changes OR there is a bug.
507 pop(title='Other:',
508 f=lambda b: (get_name(b), 'id=%s' % b['id']))
509 assert len(builds) == 0
vapiera7fbd5a2016-06-16 09:17:49 -0700510 print('Total: %d tryjobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000511
512
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000513def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
514 """Return the corresponding git ref if |base_url| together with |glob_spec|
515 matches the full |url|.
516
517 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
518 """
519 fetch_suburl, as_ref = glob_spec.split(':')
520 if allow_wildcards:
521 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
522 if glob_match:
523 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
524 # "branches/{472,597,648}/src:refs/remotes/svn/*".
525 branch_re = re.escape(base_url)
526 if glob_match.group(1):
527 branch_re += '/' + re.escape(glob_match.group(1))
528 wildcard = glob_match.group(2)
529 if wildcard == '*':
530 branch_re += '([^/]*)'
531 else:
532 # Escape and replace surrounding braces with parentheses and commas
533 # with pipe symbols.
534 wildcard = re.escape(wildcard)
535 wildcard = re.sub('^\\\\{', '(', wildcard)
536 wildcard = re.sub('\\\\,', '|', wildcard)
537 wildcard = re.sub('\\\\}$', ')', wildcard)
538 branch_re += wildcard
539 if glob_match.group(3):
540 branch_re += re.escape(glob_match.group(3))
541 match = re.match(branch_re, url)
542 if match:
543 return re.sub('\*$', match.group(1), as_ref)
544
545 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
546 if fetch_suburl:
547 full_url = base_url + '/' + fetch_suburl
548 else:
549 full_url = base_url
550 if full_url == url:
551 return as_ref
552 return None
553
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000554
iannucci@chromium.org79540052012-10-19 23:15:26 +0000555def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000556 """Prints statistics about the change to the user."""
557 # --no-ext-diff is broken in some versions of Git, so try to work around
558 # this by overriding the environment (but there is still a problem if the
559 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000560 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000561 if 'GIT_EXTERNAL_DIFF' in env:
562 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000563
564 if find_copies:
565 similarity_options = ['--find-copies-harder', '-l100000',
566 '-C%s' % similarity]
567 else:
568 similarity_options = ['-M%s' % similarity]
569
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000570 try:
571 stdout = sys.stdout.fileno()
572 except AttributeError:
573 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000574 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000575 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000576 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000577 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000578
579
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000580class BuildbucketResponseException(Exception):
581 pass
582
583
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000584class Settings(object):
585 def __init__(self):
586 self.default_server = None
587 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000588 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000589 self.is_git_svn = None
590 self.svn_branch = None
591 self.tree_status_url = None
592 self.viewvc_url = None
593 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000594 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000595 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000596 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000597 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000598 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000599 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000600 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000601
602 def LazyUpdateIfNeeded(self):
603 """Updates the settings from a codereview.settings file, if available."""
604 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000605 # The only value that actually changes the behavior is
606 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000607 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000608 error_ok=True
609 ).strip().lower()
610
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000611 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000612 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000613 LoadCodereviewSettingsFromFile(cr_settings_file)
614 self.updated = True
615
616 def GetDefaultServerUrl(self, error_ok=False):
617 if not self.default_server:
618 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000619 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000620 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000621 if error_ok:
622 return self.default_server
623 if not self.default_server:
624 error_message = ('Could not find settings file. You must configure '
625 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000626 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000627 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000628 return self.default_server
629
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000630 @staticmethod
631 def GetRelativeRoot():
632 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000633
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000634 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000635 if self.root is None:
636 self.root = os.path.abspath(self.GetRelativeRoot())
637 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000638
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000639 def GetGitMirror(self, remote='origin'):
640 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000641 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000642 if not os.path.isdir(local_url):
643 return None
644 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
645 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
646 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
647 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
648 if mirror.exists():
649 return mirror
650 return None
651
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000652 def GetIsGitSvn(self):
653 """Return true if this repo looks like it's using git-svn."""
654 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000655 if self.GetPendingRefPrefix():
656 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
657 self.is_git_svn = False
658 else:
659 # If you have any "svn-remote.*" config keys, we think you're using svn.
660 self.is_git_svn = RunGitWithCode(
661 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000662 return self.is_git_svn
663
664 def GetSVNBranch(self):
665 if self.svn_branch is None:
666 if not self.GetIsGitSvn():
667 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
668
669 # Try to figure out which remote branch we're based on.
670 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000671 # 1) iterate through our branch history and find the svn URL.
672 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000673
674 # regexp matching the git-svn line that contains the URL.
675 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
676
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000677 # We don't want to go through all of history, so read a line from the
678 # pipe at a time.
679 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000680 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000681 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
682 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000683 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000684 for line in proc.stdout:
685 match = git_svn_re.match(line)
686 if match:
687 url = match.group(1)
688 proc.stdout.close() # Cut pipe.
689 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000690
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000691 if url:
692 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
693 remotes = RunGit(['config', '--get-regexp',
694 r'^svn-remote\..*\.url']).splitlines()
695 for remote in remotes:
696 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000697 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000698 remote = match.group(1)
699 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000700 rewrite_root = RunGit(
701 ['config', 'svn-remote.%s.rewriteRoot' % remote],
702 error_ok=True).strip()
703 if rewrite_root:
704 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000705 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000706 ['config', 'svn-remote.%s.fetch' % remote],
707 error_ok=True).strip()
708 if fetch_spec:
709 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
710 if self.svn_branch:
711 break
712 branch_spec = RunGit(
713 ['config', 'svn-remote.%s.branches' % remote],
714 error_ok=True).strip()
715 if branch_spec:
716 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
717 if self.svn_branch:
718 break
719 tag_spec = RunGit(
720 ['config', 'svn-remote.%s.tags' % remote],
721 error_ok=True).strip()
722 if tag_spec:
723 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
724 if self.svn_branch:
725 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000726
727 if not self.svn_branch:
728 DieWithError('Can\'t guess svn branch -- try specifying it on the '
729 'command line')
730
731 return self.svn_branch
732
733 def GetTreeStatusUrl(self, error_ok=False):
734 if not self.tree_status_url:
735 error_message = ('You must configure your tree status URL by running '
736 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000737 self.tree_status_url = self._GetRietveldConfig(
738 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000739 return self.tree_status_url
740
741 def GetViewVCUrl(self):
742 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000743 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000744 return self.viewvc_url
745
rmistry@google.com90752582014-01-14 21:04:50 +0000746 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000747 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000748
rmistry@google.com78948ed2015-07-08 23:09:57 +0000749 def GetIsSkipDependencyUpload(self, branch_name):
750 """Returns true if specified branch should skip dep uploads."""
751 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
752 error_ok=True)
753
rmistry@google.com5626a922015-02-26 14:03:30 +0000754 def GetRunPostUploadHook(self):
755 run_post_upload_hook = self._GetRietveldConfig(
756 'run-post-upload-hook', error_ok=True)
757 return run_post_upload_hook == "True"
758
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000759 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000760 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000761
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000762 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000763 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000764
ukai@chromium.orge8077812012-02-03 03:41:46 +0000765 def GetIsGerrit(self):
766 """Return true if this repo is assosiated with gerrit code review system."""
767 if self.is_gerrit is None:
768 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
769 return self.is_gerrit
770
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000771 def GetSquashGerritUploads(self):
772 """Return true if uploads to Gerrit should be squashed by default."""
773 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700774 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
775 if self.squash_gerrit_uploads is None:
776 # Default is squash now (http://crbug.com/611892#c23).
777 self.squash_gerrit_uploads = not (
778 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
779 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000780 return self.squash_gerrit_uploads
781
tandriia60502f2016-06-20 02:01:53 -0700782 def GetSquashGerritUploadsOverride(self):
783 """Return True or False if codereview.settings should be overridden.
784
785 Returns None if no override has been defined.
786 """
787 # See also http://crbug.com/611892#c23
788 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
789 error_ok=True).strip()
790 if result == 'true':
791 return True
792 if result == 'false':
793 return False
794 return None
795
tandrii@chromium.org28253532016-04-14 13:46:56 +0000796 def GetGerritSkipEnsureAuthenticated(self):
797 """Return True if EnsureAuthenticated should not be done for Gerrit
798 uploads."""
799 if self.gerrit_skip_ensure_authenticated is None:
800 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000801 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000802 error_ok=True).strip() == 'true')
803 return self.gerrit_skip_ensure_authenticated
804
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000805 def GetGitEditor(self):
806 """Return the editor specified in the git config, or None if none is."""
807 if self.git_editor is None:
808 self.git_editor = self._GetConfig('core.editor', error_ok=True)
809 return self.git_editor or None
810
thestig@chromium.org44202a22014-03-11 19:22:18 +0000811 def GetLintRegex(self):
812 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
813 DEFAULT_LINT_REGEX)
814
815 def GetLintIgnoreRegex(self):
816 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
817 DEFAULT_LINT_IGNORE_REGEX)
818
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000819 def GetProject(self):
820 if not self.project:
821 self.project = self._GetRietveldConfig('project', error_ok=True)
822 return self.project
823
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000824 def GetForceHttpsCommitUrl(self):
825 if not self.force_https_commit_url:
826 self.force_https_commit_url = self._GetRietveldConfig(
827 'force-https-commit-url', error_ok=True)
828 return self.force_https_commit_url
829
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000830 def GetPendingRefPrefix(self):
831 if not self.pending_ref_prefix:
832 self.pending_ref_prefix = self._GetRietveldConfig(
833 'pending-ref-prefix', error_ok=True)
834 return self.pending_ref_prefix
835
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000836 def _GetRietveldConfig(self, param, **kwargs):
837 return self._GetConfig('rietveld.' + param, **kwargs)
838
rmistry@google.com78948ed2015-07-08 23:09:57 +0000839 def _GetBranchConfig(self, branch_name, param, **kwargs):
840 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
841
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000842 def _GetConfig(self, param, **kwargs):
843 self.LazyUpdateIfNeeded()
844 return RunGit(['config', param], **kwargs).strip()
845
846
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000847def ShortBranchName(branch):
848 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000849 return branch.replace('refs/heads/', '', 1)
850
851
852def GetCurrentBranchRef():
853 """Returns branch ref (e.g., refs/heads/master) or None."""
854 return RunGit(['symbolic-ref', 'HEAD'],
855 stderr=subprocess2.VOID, error_ok=True).strip() or None
856
857
858def GetCurrentBranch():
859 """Returns current branch or None.
860
861 For refs/heads/* branches, returns just last part. For others, full ref.
862 """
863 branchref = GetCurrentBranchRef()
864 if branchref:
865 return ShortBranchName(branchref)
866 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000867
868
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000869class _CQState(object):
870 """Enum for states of CL with respect to Commit Queue."""
871 NONE = 'none'
872 DRY_RUN = 'dry_run'
873 COMMIT = 'commit'
874
875 ALL_STATES = [NONE, DRY_RUN, COMMIT]
876
877
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000878class _ParsedIssueNumberArgument(object):
879 def __init__(self, issue=None, patchset=None, hostname=None):
880 self.issue = issue
881 self.patchset = patchset
882 self.hostname = hostname
883
884 @property
885 def valid(self):
886 return self.issue is not None
887
888
889class _RietveldParsedIssueNumberArgument(_ParsedIssueNumberArgument):
890 def __init__(self, *args, **kwargs):
891 self.patch_url = kwargs.pop('patch_url', None)
892 super(_RietveldParsedIssueNumberArgument, self).__init__(*args, **kwargs)
893
894
895def ParseIssueNumberArgument(arg):
896 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
897 fail_result = _ParsedIssueNumberArgument()
898
899 if arg.isdigit():
900 return _ParsedIssueNumberArgument(issue=int(arg))
901 if not arg.startswith('http'):
902 return fail_result
903 url = gclient_utils.UpgradeToHttps(arg)
904 try:
905 parsed_url = urlparse.urlparse(url)
906 except ValueError:
907 return fail_result
908 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
909 tmp = cls.ParseIssueURL(parsed_url)
910 if tmp is not None:
911 return tmp
912 return fail_result
913
914
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000915class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000916 """Changelist works with one changelist in local branch.
917
918 Supports two codereview backends: Rietveld or Gerrit, selected at object
919 creation.
920
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +0000921 Notes:
922 * Not safe for concurrent multi-{thread,process} use.
923 * Caches values from current branch. Therefore, re-use after branch change
924 with care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000925 """
926
927 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
928 """Create a new ChangeList instance.
929
930 If issue is given, the codereview must be given too.
931
932 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
933 Otherwise, it's decided based on current configuration of the local branch,
934 with default being 'rietveld' for backwards compatibility.
935 See _load_codereview_impl for more details.
936
937 **kwargs will be passed directly to codereview implementation.
938 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000939 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000940 global settings
941 if not settings:
942 # Happens when git_cl.py is used as a utility library.
943 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000944
945 if issue:
946 assert codereview, 'codereview must be known, if issue is known'
947
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000948 self.branchref = branchref
949 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000950 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000951 self.branch = ShortBranchName(self.branchref)
952 else:
953 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000954 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000955 self.lookedup_issue = False
956 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000957 self.has_description = False
958 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000959 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000960 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000961 self.cc = None
962 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000963 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000964
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000965 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000966 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000967 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000968 assert self._codereview_impl
969 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000970
971 def _load_codereview_impl(self, codereview=None, **kwargs):
972 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000973 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
974 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
975 self._codereview = codereview
976 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000977 return
978
979 # Automatic selection based on issue number set for a current branch.
980 # Rietveld takes precedence over Gerrit.
981 assert not self.issue
982 # Whether we find issue or not, we are doing the lookup.
983 self.lookedup_issue = True
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000984 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000985 setting = cls.IssueSetting(self.GetBranch())
986 issue = RunGit(['config', setting], error_ok=True).strip()
987 if issue:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000988 self._codereview = codereview
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000989 self._codereview_impl = cls(self, **kwargs)
990 self.issue = int(issue)
991 return
992
993 # No issue is set for this branch, so decide based on repo-wide settings.
994 return self._load_codereview_impl(
995 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
996 **kwargs)
997
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000998 def IsGerrit(self):
999 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001000
1001 def GetCCList(self):
1002 """Return the users cc'd on this CL.
1003
1004 Return is a string suitable for passing to gcl with the --cc flag.
1005 """
1006 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001007 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001008 more_cc = ','.join(self.watchers)
1009 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1010 return self.cc
1011
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001012 def GetCCListWithoutDefault(self):
1013 """Return the users cc'd on this CL excluding default ones."""
1014 if self.cc is None:
1015 self.cc = ','.join(self.watchers)
1016 return self.cc
1017
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001018 def SetWatchers(self, watchers):
1019 """Set the list of email addresses that should be cc'd based on the changed
1020 files in this CL.
1021 """
1022 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001023
1024 def GetBranch(self):
1025 """Returns the short branch name, e.g. 'master'."""
1026 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001027 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001028 if not branchref:
1029 return None
1030 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001031 self.branch = ShortBranchName(self.branchref)
1032 return self.branch
1033
1034 def GetBranchRef(self):
1035 """Returns the full branch name, e.g. 'refs/heads/master'."""
1036 self.GetBranch() # Poke the lazy loader.
1037 return self.branchref
1038
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001039 def ClearBranch(self):
1040 """Clears cached branch data of this object."""
1041 self.branch = self.branchref = None
1042
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001043 @staticmethod
1044 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001045 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001046 e.g. 'origin', 'refs/heads/master'
1047 """
1048 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001049 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
1050 error_ok=True).strip()
1051 if upstream_branch:
1052 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
1053 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001054 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1055 error_ok=True).strip()
1056 if upstream_branch:
1057 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001058 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001059 # Fall back on trying a git-svn upstream branch.
1060 if settings.GetIsGitSvn():
1061 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001062 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001063 # Else, try to guess the origin remote.
1064 remote_branches = RunGit(['branch', '-r']).split()
1065 if 'origin/master' in remote_branches:
1066 # Fall back on origin/master if it exits.
1067 remote = 'origin'
1068 upstream_branch = 'refs/heads/master'
1069 elif 'origin/trunk' in remote_branches:
1070 # Fall back on origin/trunk if it exists. Generally a shared
1071 # git-svn clone
1072 remote = 'origin'
1073 upstream_branch = 'refs/heads/trunk'
1074 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001075 DieWithError(
1076 'Unable to determine default branch to diff against.\n'
1077 'Either pass complete "git diff"-style arguments, like\n'
1078 ' git cl upload origin/master\n'
1079 'or verify this branch is set up to track another \n'
1080 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001081
1082 return remote, upstream_branch
1083
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001084 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001085 upstream_branch = self.GetUpstreamBranch()
1086 if not BranchExists(upstream_branch):
1087 DieWithError('The upstream for the current branch (%s) does not exist '
1088 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001089 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001090 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001091
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001092 def GetUpstreamBranch(self):
1093 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001094 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001095 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001096 upstream_branch = upstream_branch.replace('refs/heads/',
1097 'refs/remotes/%s/' % remote)
1098 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1099 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001100 self.upstream_branch = upstream_branch
1101 return self.upstream_branch
1102
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001103 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001104 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001105 remote, branch = None, self.GetBranch()
1106 seen_branches = set()
1107 while branch not in seen_branches:
1108 seen_branches.add(branch)
1109 remote, branch = self.FetchUpstreamTuple(branch)
1110 branch = ShortBranchName(branch)
1111 if remote != '.' or branch.startswith('refs/remotes'):
1112 break
1113 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001114 remotes = RunGit(['remote'], error_ok=True).split()
1115 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001116 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001117 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001118 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001119 logging.warning('Could not determine which remote this change is '
1120 'associated with, so defaulting to "%s". This may '
1121 'not be what you want. You may prevent this message '
1122 'by running "git svn info" as documented here: %s',
1123 self._remote,
1124 GIT_INSTRUCTIONS_URL)
1125 else:
1126 logging.warn('Could not determine which remote this change is '
1127 'associated with. You may prevent this message by '
1128 'running "git svn info" as documented here: %s',
1129 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001130 branch = 'HEAD'
1131 if branch.startswith('refs/remotes'):
1132 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001133 elif branch.startswith('refs/branch-heads/'):
1134 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001135 else:
1136 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001137 return self._remote
1138
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001139 def GitSanityChecks(self, upstream_git_obj):
1140 """Checks git repo status and ensures diff is from local commits."""
1141
sbc@chromium.org79706062015-01-14 21:18:12 +00001142 if upstream_git_obj is None:
1143 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001144 print('ERROR: unable to determine current branch (detached HEAD?)',
1145 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001146 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001147 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001148 return False
1149
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001150 # Verify the commit we're diffing against is in our current branch.
1151 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1152 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1153 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001154 print('ERROR: %s is not in the current branch. You may need to rebase '
1155 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001156 return False
1157
1158 # List the commits inside the diff, and verify they are all local.
1159 commits_in_diff = RunGit(
1160 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1161 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1162 remote_branch = remote_branch.strip()
1163 if code != 0:
1164 _, remote_branch = self.GetRemoteBranch()
1165
1166 commits_in_remote = RunGit(
1167 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1168
1169 common_commits = set(commits_in_diff) & set(commits_in_remote)
1170 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001171 print('ERROR: Your diff contains %d commits already in %s.\n'
1172 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1173 'the diff. If you are using a custom git flow, you can override'
1174 ' the reference used for this check with "git config '
1175 'gitcl.remotebranch <git-ref>".' % (
1176 len(common_commits), remote_branch, upstream_git_obj),
1177 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001178 return False
1179 return True
1180
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001181 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001182 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001183
1184 Returns None if it is not set.
1185 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001186 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
1187 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001188
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001189 def GetGitSvnRemoteUrl(self):
1190 """Return the configured git-svn remote URL parsed from git svn info.
1191
1192 Returns None if it is not set.
1193 """
1194 # URL is dependent on the current directory.
1195 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1196 if data:
1197 keys = dict(line.split(': ', 1) for line in data.splitlines()
1198 if ': ' in line)
1199 return keys.get('URL', None)
1200 return None
1201
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001202 def GetRemoteUrl(self):
1203 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1204
1205 Returns None if there is no remote.
1206 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001207 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001208 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1209
1210 # If URL is pointing to a local directory, it is probably a git cache.
1211 if os.path.isdir(url):
1212 url = RunGit(['config', 'remote.%s.url' % remote],
1213 error_ok=True,
1214 cwd=url).strip()
1215 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001216
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001217 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001218 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001219 if self.issue is None and not self.lookedup_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001220 issue = RunGit(['config',
1221 self._codereview_impl.IssueSetting(self.GetBranch())],
1222 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001223 self.issue = int(issue) or None if issue else None
1224 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001225 return self.issue
1226
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001227 def GetIssueURL(self):
1228 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001229 issue = self.GetIssue()
1230 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001231 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001232 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001233
1234 def GetDescription(self, pretty=False):
1235 if not self.has_description:
1236 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001237 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001238 self.has_description = True
1239 if pretty:
1240 wrapper = textwrap.TextWrapper()
1241 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1242 return wrapper.fill(self.description)
1243 return self.description
1244
1245 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001246 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001247 if self.patchset is None and not self.lookedup_patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001248 patchset = RunGit(['config', self._codereview_impl.PatchsetSetting()],
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001249 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001250 self.patchset = int(patchset) or None if patchset else None
1251 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001252 return self.patchset
1253
1254 def SetPatchset(self, patchset):
1255 """Set this branch's patchset. If patchset=0, clears the patchset."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001256 patchset_setting = self._codereview_impl.PatchsetSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001257 if patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001258 RunGit(['config', patchset_setting, str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001259 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001260 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001261 RunGit(['config', '--unset', patchset_setting],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001262 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001263 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001264
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001265 def SetIssue(self, issue=None):
1266 """Set this branch's issue. If issue isn't given, clears the issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001267 issue_setting = self._codereview_impl.IssueSetting(self.GetBranch())
1268 codereview_setting = self._codereview_impl.GetCodereviewServerSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001269 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001270 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001271 RunGit(['config', issue_setting, str(issue)])
1272 codereview_server = self._codereview_impl.GetCodereviewServer()
1273 if codereview_server:
1274 RunGit(['config', codereview_setting, codereview_server])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001275 else:
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001276 # Reset it regardless. It doesn't hurt.
1277 config_settings = [issue_setting, self._codereview_impl.PatchsetSetting()]
1278 for prop in (['last-upload-hash'] +
1279 self._codereview_impl._PostUnsetIssueProperties()):
1280 config_settings.append('branch.%s.%s' % (self.GetBranch(), prop))
1281 for setting in config_settings:
1282 RunGit(['config', '--unset', setting], error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001283 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001284 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001285
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001286 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001287 if not self.GitSanityChecks(upstream_branch):
1288 DieWithError('\nGit sanity check failure')
1289
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001290 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001291 if not root:
1292 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001293 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001294
1295 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001296 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001297 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001298 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001299 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001300 except subprocess2.CalledProcessError:
1301 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001302 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001303 'This branch probably doesn\'t exist anymore. To reset the\n'
1304 'tracking branch, please run\n'
1305 ' git branch --set-upstream %s trunk\n'
1306 'replacing trunk with origin/master or the relevant branch') %
1307 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001308
maruel@chromium.org52424302012-08-29 15:14:30 +00001309 issue = self.GetIssue()
1310 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001311 if issue:
1312 description = self.GetDescription()
1313 else:
1314 # If the change was never uploaded, use the log messages of all commits
1315 # up to the branch point, as git cl upload will prefill the description
1316 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001317 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1318 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001319
1320 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001321 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001322 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001323 name,
1324 description,
1325 absroot,
1326 files,
1327 issue,
1328 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001329 author,
1330 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001331
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001332 def UpdateDescription(self, description):
1333 self.description = description
1334 return self._codereview_impl.UpdateDescriptionRemote(description)
1335
1336 def RunHook(self, committing, may_prompt, verbose, change):
1337 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1338 try:
1339 return presubmit_support.DoPresubmitChecks(change, committing,
1340 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1341 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001342 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1343 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001344 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001345 DieWithError(
1346 ('%s\nMaybe your depot_tools is out of date?\n'
1347 'If all fails, contact maruel@') % e)
1348
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001349 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1350 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001351 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1352 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001353 else:
1354 # Assume url.
1355 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1356 urlparse.urlparse(issue_arg))
1357 if not parsed_issue_arg or not parsed_issue_arg.valid:
1358 DieWithError('Failed to parse issue argument "%s". '
1359 'Must be an issue number or a valid URL.' % issue_arg)
1360 return self._codereview_impl.CMDPatchWithParsedIssue(
1361 parsed_issue_arg, reject, nocommit, directory)
1362
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001363 def CMDUpload(self, options, git_diff_args, orig_args):
1364 """Uploads a change to codereview."""
1365 if git_diff_args:
1366 # TODO(ukai): is it ok for gerrit case?
1367 base_branch = git_diff_args[0]
1368 else:
1369 if self.GetBranch() is None:
1370 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1371
1372 # Default to diffing against common ancestor of upstream branch
1373 base_branch = self.GetCommonAncestorWithUpstream()
1374 git_diff_args = [base_branch, 'HEAD']
1375
1376 # Make sure authenticated to codereview before running potentially expensive
1377 # hooks. It is a fast, best efforts check. Codereview still can reject the
1378 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001379 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001380
1381 # Apply watchlists on upload.
1382 change = self.GetChange(base_branch, None)
1383 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1384 files = [f.LocalPath() for f in change.AffectedFiles()]
1385 if not options.bypass_watchlists:
1386 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1387
1388 if not options.bypass_hooks:
1389 if options.reviewers or options.tbr_owners:
1390 # Set the reviewer list now so that presubmit checks can access it.
1391 change_description = ChangeDescription(change.FullDescriptionText())
1392 change_description.update_reviewers(options.reviewers,
1393 options.tbr_owners,
1394 change)
1395 change.SetDescriptionText(change_description.description)
1396 hook_results = self.RunHook(committing=False,
1397 may_prompt=not options.force,
1398 verbose=options.verbose,
1399 change=change)
1400 if not hook_results.should_continue():
1401 return 1
1402 if not options.reviewers and hook_results.reviewers:
1403 options.reviewers = hook_results.reviewers.split(',')
1404
1405 if self.GetIssue():
1406 latest_patchset = self.GetMostRecentPatchset()
1407 local_patchset = self.GetPatchset()
1408 if (latest_patchset and local_patchset and
1409 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001410 print('The last upload made from this repository was patchset #%d but '
1411 'the most recent patchset on the server is #%d.'
1412 % (local_patchset, latest_patchset))
1413 print('Uploading will still work, but if you\'ve uploaded to this '
1414 'issue from another machine or branch the patch you\'re '
1415 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001416 ask_for_data('About to upload; enter to confirm.')
1417
1418 print_stats(options.similarity, options.find_copies, git_diff_args)
1419 ret = self.CMDUploadChange(options, git_diff_args, change)
1420 if not ret:
1421 git_set_branch_value('last-upload-hash',
1422 RunGit(['rev-parse', 'HEAD']).strip())
1423 # Run post upload hooks, if specified.
1424 if settings.GetRunPostUploadHook():
1425 presubmit_support.DoPostUploadExecuter(
1426 change,
1427 self,
1428 settings.GetRoot(),
1429 options.verbose,
1430 sys.stdout)
1431
1432 # Upload all dependencies if specified.
1433 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001434 print()
1435 print('--dependencies has been specified.')
1436 print('All dependent local branches will be re-uploaded.')
1437 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001438 # Remove the dependencies flag from args so that we do not end up in a
1439 # loop.
1440 orig_args.remove('--dependencies')
1441 ret = upload_branch_deps(self, orig_args)
1442 return ret
1443
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001444 def SetCQState(self, new_state):
1445 """Update the CQ state for latest patchset.
1446
1447 Issue must have been already uploaded and known.
1448 """
1449 assert new_state in _CQState.ALL_STATES
1450 assert self.GetIssue()
1451 return self._codereview_impl.SetCQState(new_state)
1452
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001453 # Forward methods to codereview specific implementation.
1454
1455 def CloseIssue(self):
1456 return self._codereview_impl.CloseIssue()
1457
1458 def GetStatus(self):
1459 return self._codereview_impl.GetStatus()
1460
1461 def GetCodereviewServer(self):
1462 return self._codereview_impl.GetCodereviewServer()
1463
1464 def GetApprovingReviewers(self):
1465 return self._codereview_impl.GetApprovingReviewers()
1466
1467 def GetMostRecentPatchset(self):
1468 return self._codereview_impl.GetMostRecentPatchset()
1469
1470 def __getattr__(self, attr):
1471 # This is because lots of untested code accesses Rietveld-specific stuff
1472 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001473 # on a case by case basis.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001474 return getattr(self._codereview_impl, attr)
1475
1476
1477class _ChangelistCodereviewBase(object):
1478 """Abstract base class encapsulating codereview specifics of a changelist."""
1479 def __init__(self, changelist):
1480 self._changelist = changelist # instance of Changelist
1481
1482 def __getattr__(self, attr):
1483 # Forward methods to changelist.
1484 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1485 # _RietveldChangelistImpl to avoid this hack?
1486 return getattr(self._changelist, attr)
1487
1488 def GetStatus(self):
1489 """Apply a rough heuristic to give a simple summary of an issue's review
1490 or CQ status, assuming adherence to a common workflow.
1491
1492 Returns None if no issue for this branch, or specific string keywords.
1493 """
1494 raise NotImplementedError()
1495
1496 def GetCodereviewServer(self):
1497 """Returns server URL without end slash, like "https://codereview.com"."""
1498 raise NotImplementedError()
1499
1500 def FetchDescription(self):
1501 """Fetches and returns description from the codereview server."""
1502 raise NotImplementedError()
1503
1504 def GetCodereviewServerSetting(self):
1505 """Returns git config setting for the codereview server."""
1506 raise NotImplementedError()
1507
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001508 @classmethod
1509 def IssueSetting(cls, branch):
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001510 return 'branch.%s.%s' % (branch, cls.IssueSettingSuffix())
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001511
1512 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001513 def IssueSettingSuffix(cls):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001514 """Returns name of git config setting which stores issue number for a given
1515 branch."""
1516 raise NotImplementedError()
1517
1518 def PatchsetSetting(self):
1519 """Returns name of git config setting which stores issue number."""
1520 raise NotImplementedError()
1521
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001522 def _PostUnsetIssueProperties(self):
1523 """Which branch-specific properties to erase when unsettin issue."""
1524 raise NotImplementedError()
1525
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001526 def GetRieveldObjForPresubmit(self):
1527 # This is an unfortunate Rietveld-embeddedness in presubmit.
1528 # For non-Rietveld codereviews, this probably should return a dummy object.
1529 raise NotImplementedError()
1530
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001531 def GetGerritObjForPresubmit(self):
1532 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1533 return None
1534
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001535 def UpdateDescriptionRemote(self, description):
1536 """Update the description on codereview site."""
1537 raise NotImplementedError()
1538
1539 def CloseIssue(self):
1540 """Closes the issue."""
1541 raise NotImplementedError()
1542
1543 def GetApprovingReviewers(self):
1544 """Returns a list of reviewers approving the change.
1545
1546 Note: not necessarily committers.
1547 """
1548 raise NotImplementedError()
1549
1550 def GetMostRecentPatchset(self):
1551 """Returns the most recent patchset number from the codereview site."""
1552 raise NotImplementedError()
1553
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001554 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1555 directory):
1556 """Fetches and applies the issue.
1557
1558 Arguments:
1559 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1560 reject: if True, reject the failed patch instead of switching to 3-way
1561 merge. Rietveld only.
1562 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1563 only.
1564 directory: switch to directory before applying the patch. Rietveld only.
1565 """
1566 raise NotImplementedError()
1567
1568 @staticmethod
1569 def ParseIssueURL(parsed_url):
1570 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1571 failed."""
1572 raise NotImplementedError()
1573
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001574 def EnsureAuthenticated(self, force):
1575 """Best effort check that user is authenticated with codereview server.
1576
1577 Arguments:
1578 force: whether to skip confirmation questions.
1579 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001580 raise NotImplementedError()
1581
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001582 def CMDUploadChange(self, options, args, change):
1583 """Uploads a change to codereview."""
1584 raise NotImplementedError()
1585
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001586 def SetCQState(self, new_state):
1587 """Update the CQ state for latest patchset.
1588
1589 Issue must have been already uploaded and known.
1590 """
1591 raise NotImplementedError()
1592
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001593
1594class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1595 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1596 super(_RietveldChangelistImpl, self).__init__(changelist)
1597 assert settings, 'must be initialized in _ChangelistCodereviewBase'
martiniss6eda05f2016-06-30 10:18:35 -07001598 if not rietveld_server:
1599 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001600
1601 self._rietveld_server = rietveld_server
1602 self._auth_config = auth_config
1603 self._props = None
1604 self._rpc_server = None
1605
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001606 def GetCodereviewServer(self):
1607 if not self._rietveld_server:
1608 # If we're on a branch then get the server potentially associated
1609 # with that branch.
1610 if self.GetIssue():
1611 rietveld_server_setting = self.GetCodereviewServerSetting()
1612 if rietveld_server_setting:
1613 self._rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1614 ['config', rietveld_server_setting], error_ok=True).strip())
1615 if not self._rietveld_server:
1616 self._rietveld_server = settings.GetDefaultServerUrl()
1617 return self._rietveld_server
1618
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001619 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001620 """Best effort check that user is authenticated with Rietveld server."""
1621 if self._auth_config.use_oauth2:
1622 authenticator = auth.get_authenticator_for_host(
1623 self.GetCodereviewServer(), self._auth_config)
1624 if not authenticator.has_cached_credentials():
1625 raise auth.LoginRequiredError(self.GetCodereviewServer())
1626
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001627 def FetchDescription(self):
1628 issue = self.GetIssue()
1629 assert issue
1630 try:
1631 return self.RpcServer().get_description(issue).strip()
1632 except urllib2.HTTPError as e:
1633 if e.code == 404:
1634 DieWithError(
1635 ('\nWhile fetching the description for issue %d, received a '
1636 '404 (not found)\n'
1637 'error. It is likely that you deleted this '
1638 'issue on the server. If this is the\n'
1639 'case, please run\n\n'
1640 ' git cl issue 0\n\n'
1641 'to clear the association with the deleted issue. Then run '
1642 'this command again.') % issue)
1643 else:
1644 DieWithError(
1645 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1646 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001647 print('Warning: Failed to retrieve CL description due to network '
1648 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001649 return ''
1650
1651 def GetMostRecentPatchset(self):
1652 return self.GetIssueProperties()['patchsets'][-1]
1653
1654 def GetPatchSetDiff(self, issue, patchset):
1655 return self.RpcServer().get(
1656 '/download/issue%s_%s.diff' % (issue, patchset))
1657
1658 def GetIssueProperties(self):
1659 if self._props is None:
1660 issue = self.GetIssue()
1661 if not issue:
1662 self._props = {}
1663 else:
1664 self._props = self.RpcServer().get_issue_properties(issue, True)
1665 return self._props
1666
1667 def GetApprovingReviewers(self):
1668 return get_approving_reviewers(self.GetIssueProperties())
1669
1670 def AddComment(self, message):
1671 return self.RpcServer().add_comment(self.GetIssue(), message)
1672
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001673 def GetStatus(self):
1674 """Apply a rough heuristic to give a simple summary of an issue's review
1675 or CQ status, assuming adherence to a common workflow.
1676
1677 Returns None if no issue for this branch, or one of the following keywords:
1678 * 'error' - error from review tool (including deleted issues)
1679 * 'unsent' - not sent for review
1680 * 'waiting' - waiting for review
1681 * 'reply' - waiting for owner to reply to review
1682 * 'lgtm' - LGTM from at least one approved reviewer
1683 * 'commit' - in the commit queue
1684 * 'closed' - closed
1685 """
1686 if not self.GetIssue():
1687 return None
1688
1689 try:
1690 props = self.GetIssueProperties()
1691 except urllib2.HTTPError:
1692 return 'error'
1693
1694 if props.get('closed'):
1695 # Issue is closed.
1696 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001697 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001698 # Issue is in the commit queue.
1699 return 'commit'
1700
1701 try:
1702 reviewers = self.GetApprovingReviewers()
1703 except urllib2.HTTPError:
1704 return 'error'
1705
1706 if reviewers:
1707 # Was LGTM'ed.
1708 return 'lgtm'
1709
1710 messages = props.get('messages') or []
1711
tandrii9d2c7a32016-06-22 03:42:45 -07001712 # Skip CQ messages that don't require owner's action.
1713 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1714 if 'Dry run:' in messages[-1]['text']:
1715 messages.pop()
1716 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1717 # This message always follows prior messages from CQ,
1718 # so skip this too.
1719 messages.pop()
1720 else:
1721 # This is probably a CQ messages warranting user attention.
1722 break
1723
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001724 if not messages:
1725 # No message was sent.
1726 return 'unsent'
1727 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07001728 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001729 return 'reply'
1730 return 'waiting'
1731
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001732 def UpdateDescriptionRemote(self, description):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001733 return self.RpcServer().update_description(
1734 self.GetIssue(), self.description)
1735
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001736 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001737 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001738
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001739 def SetFlag(self, flag, value):
1740 """Patchset must match."""
1741 if not self.GetPatchset():
1742 DieWithError('The patchset needs to match. Send another patchset.')
1743 try:
1744 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001745 self.GetIssue(), self.GetPatchset(), flag, value)
vapierfd77ac72016-06-16 08:33:57 -07001746 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001747 if e.code == 404:
1748 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1749 if e.code == 403:
1750 DieWithError(
1751 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1752 'match?') % (self.GetIssue(), self.GetPatchset()))
1753 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001754
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001755 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001756 """Returns an upload.RpcServer() to access this review's rietveld instance.
1757 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001758 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001759 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001760 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001761 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001762 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001763
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001764 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001765 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001766 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001767
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001768 def PatchsetSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001769 """Return the git setting that stores this change's most recent patchset."""
1770 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1771
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001772 def GetCodereviewServerSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001773 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001774 branch = self.GetBranch()
1775 if branch:
1776 return 'branch.%s.rietveldserver' % branch
1777 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001778
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001779 def _PostUnsetIssueProperties(self):
1780 """Which branch-specific properties to erase when unsetting issue."""
1781 return ['rietveldserver']
1782
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001783 def GetRieveldObjForPresubmit(self):
1784 return self.RpcServer()
1785
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001786 def SetCQState(self, new_state):
1787 props = self.GetIssueProperties()
1788 if props.get('private'):
1789 DieWithError('Cannot set-commit on private issue')
1790
1791 if new_state == _CQState.COMMIT:
1792 self.SetFlag('commit', '1')
1793 elif new_state == _CQState.NONE:
1794 self.SetFlag('commit', '0')
1795 else:
1796 raise NotImplementedError()
1797
1798
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001799 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1800 directory):
1801 # TODO(maruel): Use apply_issue.py
1802
1803 # PatchIssue should never be called with a dirty tree. It is up to the
1804 # caller to check this, but just in case we assert here since the
1805 # consequences of the caller not checking this could be dire.
1806 assert(not git_common.is_dirty_git_tree('apply'))
1807 assert(parsed_issue_arg.valid)
1808 self._changelist.issue = parsed_issue_arg.issue
1809 if parsed_issue_arg.hostname:
1810 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1811
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001812 if (isinstance(parsed_issue_arg, _RietveldParsedIssueNumberArgument) and
1813 parsed_issue_arg.patch_url):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001814 assert parsed_issue_arg.patchset
1815 patchset = parsed_issue_arg.patchset
1816 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1817 else:
1818 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1819 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1820
1821 # Switch up to the top-level directory, if necessary, in preparation for
1822 # applying the patch.
1823 top = settings.GetRelativeRoot()
1824 if top:
1825 os.chdir(top)
1826
1827 # Git patches have a/ at the beginning of source paths. We strip that out
1828 # with a sed script rather than the -p flag to patch so we can feed either
1829 # Git or svn-style patches into the same apply command.
1830 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1831 try:
1832 patch_data = subprocess2.check_output(
1833 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1834 except subprocess2.CalledProcessError:
1835 DieWithError('Git patch mungling failed.')
1836 logging.info(patch_data)
1837
1838 # We use "git apply" to apply the patch instead of "patch" so that we can
1839 # pick up file adds.
1840 # The --index flag means: also insert into the index (so we catch adds).
1841 cmd = ['git', 'apply', '--index', '-p0']
1842 if directory:
1843 cmd.extend(('--directory', directory))
1844 if reject:
1845 cmd.append('--reject')
1846 elif IsGitVersionAtLeast('1.7.12'):
1847 cmd.append('--3way')
1848 try:
1849 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
1850 stdin=patch_data, stdout=subprocess2.VOID)
1851 except subprocess2.CalledProcessError:
vapiera7fbd5a2016-06-16 09:17:49 -07001852 print('Failed to apply the patch')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001853 return 1
1854
1855 # If we had an issue, commit the current state and register the issue.
1856 if not nocommit:
1857 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
1858 'patch from issue %(i)s at patchset '
1859 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
1860 % {'i': self.GetIssue(), 'p': patchset})])
1861 self.SetIssue(self.GetIssue())
1862 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07001863 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001864 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001865 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001866 return 0
1867
1868 @staticmethod
1869 def ParseIssueURL(parsed_url):
1870 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1871 return None
1872 # Typical url: https://domain/<issue_number>[/[other]]
1873 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
1874 if match:
1875 return _RietveldParsedIssueNumberArgument(
1876 issue=int(match.group(1)),
1877 hostname=parsed_url.netloc)
1878 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
1879 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
1880 if match:
1881 return _RietveldParsedIssueNumberArgument(
1882 issue=int(match.group(1)),
1883 patchset=int(match.group(2)),
1884 hostname=parsed_url.netloc,
1885 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
1886 return None
1887
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001888 def CMDUploadChange(self, options, args, change):
1889 """Upload the patch to Rietveld."""
1890 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1891 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001892 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
1893 if options.emulate_svn_auto_props:
1894 upload_args.append('--emulate_svn_auto_props')
1895
1896 change_desc = None
1897
1898 if options.email is not None:
1899 upload_args.extend(['--email', options.email])
1900
1901 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07001902 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001903 upload_args.extend(['--title', options.title])
1904 if options.message:
1905 upload_args.extend(['--message', options.message])
1906 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07001907 print('This branch is associated with issue %s. '
1908 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001909 else:
nodirca166002016-06-27 10:59:51 -07001910 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001911 upload_args.extend(['--title', options.title])
1912 message = (options.title or options.message or
1913 CreateDescriptionFromLog(args))
1914 change_desc = ChangeDescription(message)
1915 if options.reviewers or options.tbr_owners:
1916 change_desc.update_reviewers(options.reviewers,
1917 options.tbr_owners,
1918 change)
1919 if not options.force:
1920 change_desc.prompt()
1921
1922 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07001923 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001924 return 1
1925
1926 upload_args.extend(['--message', change_desc.description])
1927 if change_desc.get_reviewers():
1928 upload_args.append('--reviewers=%s' % ','.join(
1929 change_desc.get_reviewers()))
1930 if options.send_mail:
1931 if not change_desc.get_reviewers():
1932 DieWithError("Must specify reviewers to send email.")
1933 upload_args.append('--send_mail')
1934
1935 # We check this before applying rietveld.private assuming that in
1936 # rietveld.cc only addresses which we can send private CLs to are listed
1937 # if rietveld.private is set, and so we should ignore rietveld.cc only
1938 # when --private is specified explicitly on the command line.
1939 if options.private:
1940 logging.warn('rietveld.cc is ignored since private flag is specified. '
1941 'You need to review and add them manually if necessary.')
1942 cc = self.GetCCListWithoutDefault()
1943 else:
1944 cc = self.GetCCList()
1945 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
1946 if cc:
1947 upload_args.extend(['--cc', cc])
1948
1949 if options.private or settings.GetDefaultPrivateFlag() == "True":
1950 upload_args.append('--private')
1951
1952 upload_args.extend(['--git_similarity', str(options.similarity)])
1953 if not options.find_copies:
1954 upload_args.extend(['--git_no_find_copies'])
1955
1956 # Include the upstream repo's URL in the change -- this is useful for
1957 # projects that have their source spread across multiple repos.
1958 remote_url = self.GetGitBaseUrlFromConfig()
1959 if not remote_url:
1960 if settings.GetIsGitSvn():
1961 remote_url = self.GetGitSvnRemoteUrl()
1962 else:
1963 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
1964 remote_url = '%s@%s' % (self.GetRemoteUrl(),
1965 self.GetUpstreamBranch().split('/')[-1])
1966 if remote_url:
1967 upload_args.extend(['--base_url', remote_url])
1968 remote, remote_branch = self.GetRemoteBranch()
1969 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
1970 settings.GetPendingRefPrefix())
1971 if target_ref:
1972 upload_args.extend(['--target_ref', target_ref])
1973
1974 # Look for dependent patchsets. See crbug.com/480453 for more details.
1975 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
1976 upstream_branch = ShortBranchName(upstream_branch)
1977 if remote is '.':
1978 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001979 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001980 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07001981 print()
1982 print('Skipping dependency patchset upload because git config '
1983 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
1984 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001985 else:
1986 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001987 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001988 auth_config=auth_config)
1989 branch_cl_issue_url = branch_cl.GetIssueURL()
1990 branch_cl_issue = branch_cl.GetIssue()
1991 branch_cl_patchset = branch_cl.GetPatchset()
1992 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
1993 upload_args.extend(
1994 ['--depends_on_patchset', '%s:%s' % (
1995 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001996 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001997 '\n'
1998 'The current branch (%s) is tracking a local branch (%s) with '
1999 'an associated CL.\n'
2000 'Adding %s/#ps%s as a dependency patchset.\n'
2001 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2002 branch_cl_patchset))
2003
2004 project = settings.GetProject()
2005 if project:
2006 upload_args.extend(['--project', project])
2007
2008 if options.cq_dry_run:
2009 upload_args.extend(['--cq_dry_run'])
2010
2011 try:
2012 upload_args = ['upload'] + upload_args + args
2013 logging.info('upload.RealMain(%s)', upload_args)
2014 issue, patchset = upload.RealMain(upload_args)
2015 issue = int(issue)
2016 patchset = int(patchset)
2017 except KeyboardInterrupt:
2018 sys.exit(1)
2019 except:
2020 # If we got an exception after the user typed a description for their
2021 # change, back up the description before re-raising.
2022 if change_desc:
2023 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2024 print('\nGot exception while uploading -- saving description to %s\n' %
2025 backup_path)
2026 backup_file = open(backup_path, 'w')
2027 backup_file.write(change_desc.description)
2028 backup_file.close()
2029 raise
2030
2031 if not self.GetIssue():
2032 self.SetIssue(issue)
2033 self.SetPatchset(patchset)
2034
2035 if options.use_commit_queue:
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002036 self.SetCQState(_CQState.COMMIT)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002037 return 0
2038
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002039
2040class _GerritChangelistImpl(_ChangelistCodereviewBase):
2041 def __init__(self, changelist, auth_config=None):
2042 # auth_config is Rietveld thing, kept here to preserve interface only.
2043 super(_GerritChangelistImpl, self).__init__(changelist)
2044 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002045 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002046 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002047 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002048
2049 def _GetGerritHost(self):
2050 # Lazy load of configs.
2051 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002052 if self._gerrit_host and '.' not in self._gerrit_host:
2053 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2054 # This happens for internal stuff http://crbug.com/614312.
2055 parsed = urlparse.urlparse(self.GetRemoteUrl())
2056 if parsed.scheme == 'sso':
2057 print('WARNING: using non https URLs for remote is likely broken\n'
2058 ' Your current remote is: %s' % self.GetRemoteUrl())
2059 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2060 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002061 return self._gerrit_host
2062
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002063 def _GetGitHost(self):
2064 """Returns git host to be used when uploading change to Gerrit."""
2065 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2066
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002067 def GetCodereviewServer(self):
2068 if not self._gerrit_server:
2069 # If we're on a branch then get the server potentially associated
2070 # with that branch.
2071 if self.GetIssue():
2072 gerrit_server_setting = self.GetCodereviewServerSetting()
2073 if gerrit_server_setting:
2074 self._gerrit_server = RunGit(['config', gerrit_server_setting],
2075 error_ok=True).strip()
2076 if self._gerrit_server:
2077 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
2078 if not self._gerrit_server:
2079 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2080 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002081 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002082 parts[0] = parts[0] + '-review'
2083 self._gerrit_host = '.'.join(parts)
2084 self._gerrit_server = 'https://%s' % self._gerrit_host
2085 return self._gerrit_server
2086
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002087 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00002088 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002089 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002090
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002091 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002092 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002093 if settings.GetGerritSkipEnsureAuthenticated():
2094 # For projects with unusual authentication schemes.
2095 # See http://crbug.com/603378.
2096 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002097 # Lazy-loader to identify Gerrit and Git hosts.
2098 if gerrit_util.GceAuthenticator.is_gce():
2099 return
2100 self.GetCodereviewServer()
2101 git_host = self._GetGitHost()
2102 assert self._gerrit_server and self._gerrit_host
2103 cookie_auth = gerrit_util.CookiesAuthenticator()
2104
2105 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2106 git_auth = cookie_auth.get_auth_header(git_host)
2107 if gerrit_auth and git_auth:
2108 if gerrit_auth == git_auth:
2109 return
2110 print((
2111 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2112 ' Check your %s or %s file for credentials of hosts:\n'
2113 ' %s\n'
2114 ' %s\n'
2115 ' %s') %
2116 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2117 git_host, self._gerrit_host,
2118 cookie_auth.get_new_password_message(git_host)))
2119 if not force:
2120 ask_for_data('If you know what you are doing, press Enter to continue, '
2121 'Ctrl+C to abort.')
2122 return
2123 else:
2124 missing = (
2125 [] if gerrit_auth else [self._gerrit_host] +
2126 [] if git_auth else [git_host])
2127 DieWithError('Credentials for the following hosts are required:\n'
2128 ' %s\n'
2129 'These are read from %s (or legacy %s)\n'
2130 '%s' % (
2131 '\n '.join(missing),
2132 cookie_auth.get_gitcookies_path(),
2133 cookie_auth.get_netrc_path(),
2134 cookie_auth.get_new_password_message(git_host)))
2135
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002136
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002137 def PatchsetSetting(self):
2138 """Return the git setting that stores this change's most recent patchset."""
2139 return 'branch.%s.gerritpatchset' % self.GetBranch()
2140
2141 def GetCodereviewServerSetting(self):
2142 """Returns the git setting that stores this change's Gerrit server."""
2143 branch = self.GetBranch()
2144 if branch:
2145 return 'branch.%s.gerritserver' % branch
2146 return None
2147
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002148 def _PostUnsetIssueProperties(self):
2149 """Which branch-specific properties to erase when unsetting issue."""
2150 return [
2151 'gerritserver',
2152 'gerritsquashhash',
2153 ]
2154
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002155 def GetRieveldObjForPresubmit(self):
2156 class ThisIsNotRietveldIssue(object):
2157 def __nonzero__(self):
2158 # This is a hack to make presubmit_support think that rietveld is not
2159 # defined, yet still ensure that calls directly result in a decent
2160 # exception message below.
2161 return False
2162
2163 def __getattr__(self, attr):
2164 print(
2165 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2166 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2167 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2168 'or use Rietveld for codereview.\n'
2169 'See also http://crbug.com/579160.' % attr)
2170 raise NotImplementedError()
2171 return ThisIsNotRietveldIssue()
2172
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002173 def GetGerritObjForPresubmit(self):
2174 return presubmit_support.GerritAccessor(self._GetGerritHost())
2175
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002176 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002177 """Apply a rough heuristic to give a simple summary of an issue's review
2178 or CQ status, assuming adherence to a common workflow.
2179
2180 Returns None if no issue for this branch, or one of the following keywords:
2181 * 'error' - error from review tool (including deleted issues)
2182 * 'unsent' - no reviewers added
2183 * 'waiting' - waiting for review
2184 * 'reply' - waiting for owner to reply to review
2185 * 'not lgtm' - Code-Review -2 from at least one approved reviewer
2186 * 'lgtm' - Code-Review +2 from at least one approved reviewer
2187 * 'commit' - in the commit queue
2188 * 'closed' - abandoned
2189 """
2190 if not self.GetIssue():
2191 return None
2192
2193 try:
2194 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
2195 except httplib.HTTPException:
2196 return 'error'
2197
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002198 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002199 return 'closed'
2200
2201 cq_label = data['labels'].get('Commit-Queue', {})
2202 if cq_label:
2203 # Vote value is a stringified integer, which we expect from 0 to 2.
2204 vote_value = cq_label.get('value', '0')
2205 vote_text = cq_label.get('values', {}).get(vote_value, '')
2206 if vote_text.lower() == 'commit':
2207 return 'commit'
2208
2209 lgtm_label = data['labels'].get('Code-Review', {})
2210 if lgtm_label:
2211 if 'rejected' in lgtm_label:
2212 return 'not lgtm'
2213 if 'approved' in lgtm_label:
2214 return 'lgtm'
2215
2216 if not data.get('reviewers', {}).get('REVIEWER', []):
2217 return 'unsent'
2218
2219 messages = data.get('messages', [])
2220 if messages:
2221 owner = data['owner'].get('_account_id')
2222 last_message_author = messages[-1].get('author', {}).get('_account_id')
2223 if owner != last_message_author:
2224 # Some reply from non-owner.
2225 return 'reply'
2226
2227 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002228
2229 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002230 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002231 return data['revisions'][data['current_revision']]['_number']
2232
2233 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002234 data = self._GetChangeDetail(['CURRENT_REVISION'])
2235 current_rev = data['current_revision']
2236 url = data['revisions'][current_rev]['fetch']['http']['url']
2237 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002238
2239 def UpdateDescriptionRemote(self, description):
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002240 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
2241 description)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002242
2243 def CloseIssue(self):
2244 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2245
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002246 def GetApprovingReviewers(self):
2247 """Returns a list of reviewers approving the change.
2248
2249 Note: not necessarily committers.
2250 """
2251 raise NotImplementedError()
2252
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002253 def SubmitIssue(self, wait_for_merge=True):
2254 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2255 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002256
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002257 def _GetChangeDetail(self, options=None, issue=None):
2258 options = options or []
2259 issue = issue or self.GetIssue()
2260 assert issue, 'issue required to query Gerrit'
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002261 return gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2262 options)
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002263
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002264 def CMDLand(self, force, bypass_hooks, verbose):
2265 if git_common.is_dirty_git_tree('land'):
2266 return 1
tandriid60367b2016-06-22 05:25:12 -07002267 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2268 if u'Commit-Queue' in detail.get('labels', {}):
2269 if not force:
2270 ask_for_data('\nIt seems this repository has a Commit Queue, '
2271 'which can test and land changes for you. '
2272 'Are you sure you wish to bypass it?\n'
2273 'Press Enter to continue, Ctrl+C to abort.')
2274
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002275 differs = True
2276 last_upload = RunGit(['config',
2277 'branch.%s.gerritsquashhash' % self.GetBranch()],
2278 error_ok=True).strip()
2279 # Note: git diff outputs nothing if there is no diff.
2280 if not last_upload or RunGit(['diff', last_upload]).strip():
2281 print('WARNING: some changes from local branch haven\'t been uploaded')
2282 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002283 if detail['current_revision'] == last_upload:
2284 differs = False
2285 else:
2286 print('WARNING: local branch contents differ from latest uploaded '
2287 'patchset')
2288 if differs:
2289 if not force:
2290 ask_for_data(
2291 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2292 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2293 elif not bypass_hooks:
2294 hook_results = self.RunHook(
2295 committing=True,
2296 may_prompt=not force,
2297 verbose=verbose,
2298 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2299 if not hook_results.should_continue():
2300 return 1
2301
2302 self.SubmitIssue(wait_for_merge=True)
2303 print('Issue %s has been submitted.' % self.GetIssueURL())
2304 return 0
2305
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002306 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2307 directory):
2308 assert not reject
2309 assert not nocommit
2310 assert not directory
2311 assert parsed_issue_arg.valid
2312
2313 self._changelist.issue = parsed_issue_arg.issue
2314
2315 if parsed_issue_arg.hostname:
2316 self._gerrit_host = parsed_issue_arg.hostname
2317 self._gerrit_server = 'https://%s' % self._gerrit_host
2318
2319 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2320
2321 if not parsed_issue_arg.patchset:
2322 # Use current revision by default.
2323 revision_info = detail['revisions'][detail['current_revision']]
2324 patchset = int(revision_info['_number'])
2325 else:
2326 patchset = parsed_issue_arg.patchset
2327 for revision_info in detail['revisions'].itervalues():
2328 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2329 break
2330 else:
2331 DieWithError('Couldn\'t find patchset %i in issue %i' %
2332 (parsed_issue_arg.patchset, self.GetIssue()))
2333
2334 fetch_info = revision_info['fetch']['http']
2335 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2336 RunGit(['cherry-pick', 'FETCH_HEAD'])
2337 self.SetIssue(self.GetIssue())
2338 self.SetPatchset(patchset)
2339 print('Committed patch for issue %i pathset %i locally' %
2340 (self.GetIssue(), self.GetPatchset()))
2341 return 0
2342
2343 @staticmethod
2344 def ParseIssueURL(parsed_url):
2345 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2346 return None
2347 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2348 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2349 # Short urls like https://domain/<issue_number> can be used, but don't allow
2350 # specifying the patchset (you'd 404), but we allow that here.
2351 if parsed_url.path == '/':
2352 part = parsed_url.fragment
2353 else:
2354 part = parsed_url.path
2355 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2356 if match:
2357 return _ParsedIssueNumberArgument(
2358 issue=int(match.group(2)),
2359 patchset=int(match.group(4)) if match.group(4) else None,
2360 hostname=parsed_url.netloc)
2361 return None
2362
tandrii16e0b4e2016-06-07 10:34:28 -07002363 def _GerritCommitMsgHookCheck(self, offer_removal):
2364 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2365 if not os.path.exists(hook):
2366 return
2367 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2368 # custom developer made one.
2369 data = gclient_utils.FileRead(hook)
2370 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2371 return
2372 print('Warning: you have Gerrit commit-msg hook installed.\n'
2373 'It is not neccessary for uploading with git cl in squash mode, '
2374 'and may interfere with it in subtle ways.\n'
2375 'We recommend you remove the commit-msg hook.')
2376 if offer_removal:
2377 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2378 if reply.lower().startswith('y'):
2379 gclient_utils.rm_file_or_tree(hook)
2380 print('Gerrit commit-msg hook removed.')
2381 else:
2382 print('OK, will keep Gerrit commit-msg hook in place.')
2383
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002384 def CMDUploadChange(self, options, args, change):
2385 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002386 if options.squash and options.no_squash:
2387 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002388
2389 if not options.squash and not options.no_squash:
2390 # Load default for user, repo, squash=true, in this order.
2391 options.squash = settings.GetSquashGerritUploads()
2392 elif options.no_squash:
2393 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002394
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002395 # We assume the remote called "origin" is the one we want.
2396 # It is probably not worthwhile to support different workflows.
2397 gerrit_remote = 'origin'
2398
2399 remote, remote_branch = self.GetRemoteBranch()
2400 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2401 pending_prefix='')
2402
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002403 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002404 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002405 if self.GetIssue():
2406 # Try to get the message from a previous upload.
2407 message = self.GetDescription()
2408 if not message:
2409 DieWithError(
2410 'failed to fetch description from current Gerrit issue %d\n'
2411 '%s' % (self.GetIssue(), self.GetIssueURL()))
2412 change_id = self._GetChangeDetail()['change_id']
2413 while True:
2414 footer_change_ids = git_footers.get_footer_change_id(message)
2415 if footer_change_ids == [change_id]:
2416 break
2417 if not footer_change_ids:
2418 message = git_footers.add_footer_change_id(message, change_id)
2419 print('WARNING: appended missing Change-Id to issue description')
2420 continue
2421 # There is already a valid footer but with different or several ids.
2422 # Doing this automatically is non-trivial as we don't want to lose
2423 # existing other footers, yet we want to append just 1 desired
2424 # Change-Id. Thus, just create a new footer, but let user verify the
2425 # new description.
2426 message = '%s\n\nChange-Id: %s' % (message, change_id)
2427 print(
2428 'WARNING: issue %s has Change-Id footer(s):\n'
2429 ' %s\n'
2430 'but issue has Change-Id %s, according to Gerrit.\n'
2431 'Please, check the proposed correction to the description, '
2432 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2433 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2434 change_id))
2435 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2436 if not options.force:
2437 change_desc = ChangeDescription(message)
2438 change_desc.prompt()
2439 message = change_desc.description
2440 if not message:
2441 DieWithError("Description is empty. Aborting...")
2442 # Continue the while loop.
2443 # Sanity check of this code - we should end up with proper message
2444 # footer.
2445 assert [change_id] == git_footers.get_footer_change_id(message)
2446 change_desc = ChangeDescription(message)
2447 else:
2448 change_desc = ChangeDescription(
2449 options.message or CreateDescriptionFromLog(args))
2450 if not options.force:
2451 change_desc.prompt()
2452 if not change_desc.description:
2453 DieWithError("Description is empty. Aborting...")
2454 message = change_desc.description
2455 change_ids = git_footers.get_footer_change_id(message)
2456 if len(change_ids) > 1:
2457 DieWithError('too many Change-Id footers, at most 1 allowed.')
2458 if not change_ids:
2459 # Generate the Change-Id automatically.
2460 message = git_footers.add_footer_change_id(
2461 message, GenerateGerritChangeId(message))
2462 change_desc.set_description(message)
2463 change_ids = git_footers.get_footer_change_id(message)
2464 assert len(change_ids) == 1
2465 change_id = change_ids[0]
2466
2467 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2468 if remote is '.':
2469 # If our upstream branch is local, we base our squashed commit on its
2470 # squashed version.
2471 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2472 # Check the squashed hash of the parent.
2473 parent = RunGit(['config',
2474 'branch.%s.gerritsquashhash' % upstream_branch_name],
2475 error_ok=True).strip()
2476 # Verify that the upstream branch has been uploaded too, otherwise
2477 # Gerrit will create additional CLs when uploading.
2478 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2479 RunGitSilent(['rev-parse', parent + ':'])):
2480 # TODO(tandrii): remove "old depot_tools" part on April 12, 2016.
2481 DieWithError(
2482 'Upload upstream branch %s first.\n'
2483 'Note: maybe you\'ve uploaded it with --no-squash or with an old '
2484 'version of depot_tools. If so, then re-upload it with:\n'
2485 ' git cl upload --squash\n' % upstream_branch_name)
2486 else:
2487 parent = self.GetCommonAncestorWithUpstream()
2488
2489 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2490 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2491 '-m', message]).strip()
2492 else:
2493 change_desc = ChangeDescription(
2494 options.message or CreateDescriptionFromLog(args))
2495 if not change_desc.description:
2496 DieWithError("Description is empty. Aborting...")
2497
2498 if not git_footers.get_footer_change_id(change_desc.description):
2499 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002500 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2501 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002502 ref_to_push = 'HEAD'
2503 parent = '%s/%s' % (gerrit_remote, branch)
2504 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2505
2506 assert change_desc
2507 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2508 ref_to_push)]).splitlines()
2509 if len(commits) > 1:
2510 print('WARNING: This will upload %d commits. Run the following command '
2511 'to see which commits will be uploaded: ' % len(commits))
2512 print('git log %s..%s' % (parent, ref_to_push))
2513 print('You can also use `git squash-branch` to squash these into a '
2514 'single commit.')
2515 ask_for_data('About to upload; enter to confirm.')
2516
2517 if options.reviewers or options.tbr_owners:
2518 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2519 change)
2520
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002521 # Extra options that can be specified at push time. Doc:
2522 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2523 refspec_opts = []
2524 if options.title:
2525 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2526 # reverse on its side.
2527 if '_' in options.title:
2528 print('WARNING: underscores in title will be converted to spaces.')
2529 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2530
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002531 if options.send_mail:
2532 if not change_desc.get_reviewers():
2533 DieWithError('Must specify reviewers to send email.')
2534 refspec_opts.append('notify=ALL')
2535 else:
2536 refspec_opts.append('notify=NONE')
2537
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002538 cc = self.GetCCList().split(',')
2539 if options.cc:
2540 cc.extend(options.cc)
2541 cc = filter(None, cc)
2542 if cc:
tandrii@chromium.org074c2af2016-06-03 23:18:40 +00002543 refspec_opts.extend('cc=' + email.strip() for email in cc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002544
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002545 if change_desc.get_reviewers():
2546 refspec_opts.extend('r=' + email.strip()
2547 for email in change_desc.get_reviewers())
2548
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002549 refspec_suffix = ''
2550 if refspec_opts:
2551 refspec_suffix = '%' + ','.join(refspec_opts)
2552 assert ' ' not in refspec_suffix, (
2553 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002554 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002555
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002556 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002557 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002558 print_stdout=True,
2559 # Flush after every line: useful for seeing progress when running as
2560 # recipe.
2561 filter_fn=lambda _: sys.stdout.flush())
2562
2563 if options.squash:
2564 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2565 change_numbers = [m.group(1)
2566 for m in map(regex.match, push_stdout.splitlines())
2567 if m]
2568 if len(change_numbers) != 1:
2569 DieWithError(
2570 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2571 'Change-Id: %s') % (len(change_numbers), change_id))
2572 self.SetIssue(change_numbers[0])
2573 RunGit(['config', 'branch.%s.gerritsquashhash' % self.GetBranch(),
2574 ref_to_push])
2575 return 0
2576
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002577 def _AddChangeIdToCommitMessage(self, options, args):
2578 """Re-commits using the current message, assumes the commit hook is in
2579 place.
2580 """
2581 log_desc = options.message or CreateDescriptionFromLog(args)
2582 git_command = ['commit', '--amend', '-m', log_desc]
2583 RunGit(git_command)
2584 new_log_desc = CreateDescriptionFromLog(args)
2585 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002586 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002587 return new_log_desc
2588 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002589 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002590
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002591 def SetCQState(self, new_state):
2592 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
2593 # TODO(tandrii): maybe allow configurability in codereview.settings or by
2594 # self-discovery of label config for this CL using REST API.
2595 vote_map = {
2596 _CQState.NONE: 0,
2597 _CQState.DRY_RUN: 1,
2598 _CQState.COMMIT : 2,
2599 }
2600 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2601 labels={'Commit-Queue': vote_map[new_state]})
2602
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002603
2604_CODEREVIEW_IMPLEMENTATIONS = {
2605 'rietveld': _RietveldChangelistImpl,
2606 'gerrit': _GerritChangelistImpl,
2607}
2608
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002609
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002610def _add_codereview_select_options(parser):
2611 """Appends --gerrit and --rietveld options to force specific codereview."""
2612 parser.codereview_group = optparse.OptionGroup(
2613 parser, 'EXPERIMENTAL! Codereview override options')
2614 parser.add_option_group(parser.codereview_group)
2615 parser.codereview_group.add_option(
2616 '--gerrit', action='store_true',
2617 help='Force the use of Gerrit for codereview')
2618 parser.codereview_group.add_option(
2619 '--rietveld', action='store_true',
2620 help='Force the use of Rietveld for codereview')
2621
2622
2623def _process_codereview_select_options(parser, options):
2624 if options.gerrit and options.rietveld:
2625 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2626 options.forced_codereview = None
2627 if options.gerrit:
2628 options.forced_codereview = 'gerrit'
2629 elif options.rietveld:
2630 options.forced_codereview = 'rietveld'
2631
2632
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002633class ChangeDescription(object):
2634 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002635 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002636 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002637
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002638 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002639 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002640
agable@chromium.org42c20792013-09-12 17:34:49 +00002641 @property # www.logilab.org/ticket/89786
2642 def description(self): # pylint: disable=E0202
2643 return '\n'.join(self._description_lines)
2644
2645 def set_description(self, desc):
2646 if isinstance(desc, basestring):
2647 lines = desc.splitlines()
2648 else:
2649 lines = [line.rstrip() for line in desc]
2650 while lines and not lines[0]:
2651 lines.pop(0)
2652 while lines and not lines[-1]:
2653 lines.pop(-1)
2654 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002655
piman@chromium.org336f9122014-09-04 02:16:55 +00002656 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002657 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002658 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002659 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002660 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002661 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002662
agable@chromium.org42c20792013-09-12 17:34:49 +00002663 # Get the set of R= and TBR= lines and remove them from the desciption.
2664 regexp = re.compile(self.R_LINE)
2665 matches = [regexp.match(line) for line in self._description_lines]
2666 new_desc = [l for i, l in enumerate(self._description_lines)
2667 if not matches[i]]
2668 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002669
agable@chromium.org42c20792013-09-12 17:34:49 +00002670 # Construct new unified R= and TBR= lines.
2671 r_names = []
2672 tbr_names = []
2673 for match in matches:
2674 if not match:
2675 continue
2676 people = cleanup_list([match.group(2).strip()])
2677 if match.group(1) == 'TBR':
2678 tbr_names.extend(people)
2679 else:
2680 r_names.extend(people)
2681 for name in r_names:
2682 if name not in reviewers:
2683 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002684 if add_owners_tbr:
2685 owners_db = owners.Database(change.RepositoryRoot(),
2686 fopen=file, os_path=os.path, glob=glob.glob)
2687 all_reviewers = set(tbr_names + reviewers)
2688 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2689 all_reviewers)
2690 tbr_names.extend(owners_db.reviewers_for(missing_files,
2691 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002692 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2693 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2694
2695 # Put the new lines in the description where the old first R= line was.
2696 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2697 if 0 <= line_loc < len(self._description_lines):
2698 if new_tbr_line:
2699 self._description_lines.insert(line_loc, new_tbr_line)
2700 if new_r_line:
2701 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002702 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002703 if new_r_line:
2704 self.append_footer(new_r_line)
2705 if new_tbr_line:
2706 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002707
2708 def prompt(self):
2709 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002710 self.set_description([
2711 '# Enter a description of the change.',
2712 '# This will be displayed on the codereview site.',
2713 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002714 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002715 '--------------------',
2716 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002717
agable@chromium.org42c20792013-09-12 17:34:49 +00002718 regexp = re.compile(self.BUG_LINE)
2719 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00002720 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00002721 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002722 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002723 if not content:
2724 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002725 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002726
2727 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002728 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2729 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002730 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002731 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002732
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002733 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00002734 """Adds a footer line to the description.
2735
2736 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
2737 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
2738 that Gerrit footers are always at the end.
2739 """
2740 parsed_footer_line = git_footers.parse_footer(line)
2741 if parsed_footer_line:
2742 # Line is a gerrit footer in the form: Footer-Key: any value.
2743 # Thus, must be appended observing Gerrit footer rules.
2744 self.set_description(
2745 git_footers.add_footer(self.description,
2746 key=parsed_footer_line[0],
2747 value=parsed_footer_line[1]))
2748 return
2749
2750 if not self._description_lines:
2751 self._description_lines.append(line)
2752 return
2753
2754 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
2755 if gerrit_footers:
2756 # git_footers.split_footers ensures that there is an empty line before
2757 # actual (gerrit) footers, if any. We have to keep it that way.
2758 assert top_lines and top_lines[-1] == ''
2759 top_lines, separator = top_lines[:-1], top_lines[-1:]
2760 else:
2761 separator = [] # No need for separator if there are no gerrit_footers.
2762
2763 prev_line = top_lines[-1] if top_lines else ''
2764 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
2765 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2766 top_lines.append('')
2767 top_lines.append(line)
2768 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002769
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002770 def get_reviewers(self):
2771 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002772 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
2773 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002774 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002775
2776
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002777def get_approving_reviewers(props):
2778 """Retrieves the reviewers that approved a CL from the issue properties with
2779 messages.
2780
2781 Note that the list may contain reviewers that are not committer, thus are not
2782 considered by the CQ.
2783 """
2784 return sorted(
2785 set(
2786 message['sender']
2787 for message in props['messages']
2788 if message['approval'] and message['sender'] in props['reviewers']
2789 )
2790 )
2791
2792
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002793def FindCodereviewSettingsFile(filename='codereview.settings'):
2794 """Finds the given file starting in the cwd and going up.
2795
2796 Only looks up to the top of the repository unless an
2797 'inherit-review-settings-ok' file exists in the root of the repository.
2798 """
2799 inherit_ok_file = 'inherit-review-settings-ok'
2800 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002801 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002802 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2803 root = '/'
2804 while True:
2805 if filename in os.listdir(cwd):
2806 if os.path.isfile(os.path.join(cwd, filename)):
2807 return open(os.path.join(cwd, filename))
2808 if cwd == root:
2809 break
2810 cwd = os.path.dirname(cwd)
2811
2812
2813def LoadCodereviewSettingsFromFile(fileobj):
2814 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002815 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002816
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002817 def SetProperty(name, setting, unset_error_ok=False):
2818 fullname = 'rietveld.' + name
2819 if setting in keyvals:
2820 RunGit(['config', fullname, keyvals[setting]])
2821 else:
2822 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2823
2824 SetProperty('server', 'CODE_REVIEW_SERVER')
2825 # Only server setting is required. Other settings can be absent.
2826 # In that case, we ignore errors raised during option deletion attempt.
2827 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002828 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002829 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2830 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002831 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002832 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002833 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2834 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002835 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002836 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002837 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002838 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2839 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002840
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002841 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002842 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002843
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002844 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07002845 RunGit(['config', 'gerrit.squash-uploads',
2846 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002847
tandrii@chromium.org28253532016-04-14 13:46:56 +00002848 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00002849 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00002850 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
2851
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002852 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2853 #should be of the form
2854 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2855 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2856 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2857 keyvals['ORIGIN_URL_CONFIG']])
2858
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002859
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002860def urlretrieve(source, destination):
2861 """urllib is broken for SSL connections via a proxy therefore we
2862 can't use urllib.urlretrieve()."""
2863 with open(destination, 'w') as f:
2864 f.write(urllib2.urlopen(source).read())
2865
2866
ukai@chromium.org712d6102013-11-27 00:52:58 +00002867def hasSheBang(fname):
2868 """Checks fname is a #! script."""
2869 with open(fname) as f:
2870 return f.read(2).startswith('#!')
2871
2872
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00002873# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
2874def DownloadHooks(*args, **kwargs):
2875 pass
2876
2877
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002878def DownloadGerritHook(force):
2879 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002880
2881 Args:
2882 force: True to update hooks. False to install hooks if not present.
2883 """
2884 if not settings.GetIsGerrit():
2885 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00002886 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002887 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2888 if not os.access(dst, os.X_OK):
2889 if os.path.exists(dst):
2890 if not force:
2891 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002892 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002893 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002894 if not hasSheBang(dst):
2895 DieWithError('Not a script: %s\n'
2896 'You need to download from\n%s\n'
2897 'into .git/hooks/commit-msg and '
2898 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002899 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2900 except Exception:
2901 if os.path.exists(dst):
2902 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002903 DieWithError('\nFailed to download hooks.\n'
2904 'You need to download from\n%s\n'
2905 'into .git/hooks/commit-msg and '
2906 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002907
2908
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002909
2910def GetRietveldCodereviewSettingsInteractively():
2911 """Prompt the user for settings."""
2912 server = settings.GetDefaultServerUrl(error_ok=True)
2913 prompt = 'Rietveld server (host[:port])'
2914 prompt += ' [%s]' % (server or DEFAULT_SERVER)
2915 newserver = ask_for_data(prompt + ':')
2916 if not server and not newserver:
2917 newserver = DEFAULT_SERVER
2918 if newserver:
2919 newserver = gclient_utils.UpgradeToHttps(newserver)
2920 if newserver != server:
2921 RunGit(['config', 'rietveld.server', newserver])
2922
2923 def SetProperty(initial, caption, name, is_url):
2924 prompt = caption
2925 if initial:
2926 prompt += ' ("x" to clear) [%s]' % initial
2927 new_val = ask_for_data(prompt + ':')
2928 if new_val == 'x':
2929 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
2930 elif new_val:
2931 if is_url:
2932 new_val = gclient_utils.UpgradeToHttps(new_val)
2933 if new_val != initial:
2934 RunGit(['config', 'rietveld.' + name, new_val])
2935
2936 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
2937 SetProperty(settings.GetDefaultPrivateFlag(),
2938 'Private flag (rietveld only)', 'private', False)
2939 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
2940 'tree-status-url', False)
2941 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
2942 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
2943 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
2944 'run-post-upload-hook', False)
2945
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002946@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002947def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002948 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002949
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002950 print('WARNING: git cl config works for Rietveld only.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002951 'For Gerrit, see http://crbug.com/603116.')
2952 # TODO(tandrii): add Gerrit support as part of http://crbug.com/603116.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00002953 parser.add_option('--activate-update', action='store_true',
2954 help='activate auto-updating [rietveld] section in '
2955 '.git/config')
2956 parser.add_option('--deactivate-update', action='store_true',
2957 help='deactivate auto-updating [rietveld] section in '
2958 '.git/config')
2959 options, args = parser.parse_args(args)
2960
2961 if options.deactivate_update:
2962 RunGit(['config', 'rietveld.autoupdate', 'false'])
2963 return
2964
2965 if options.activate_update:
2966 RunGit(['config', '--unset', 'rietveld.autoupdate'])
2967 return
2968
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002969 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002970 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002971 return 0
2972
2973 url = args[0]
2974 if not url.endswith('codereview.settings'):
2975 url = os.path.join(url, 'codereview.settings')
2976
2977 # Load code review settings and download hooks (if available).
2978 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
2979 return 0
2980
2981
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002982def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002983 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002984 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
2985 branch = ShortBranchName(branchref)
2986 _, args = parser.parse_args(args)
2987 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07002988 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002989 return RunGit(['config', 'branch.%s.base-url' % branch],
2990 error_ok=False).strip()
2991 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002992 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002993 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
2994 error_ok=False).strip()
2995
2996
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002997def color_for_status(status):
2998 """Maps a Changelist status to color, for CMDstatus and other tools."""
2999 return {
3000 'unsent': Fore.RED,
3001 'waiting': Fore.BLUE,
3002 'reply': Fore.YELLOW,
3003 'lgtm': Fore.GREEN,
3004 'commit': Fore.MAGENTA,
3005 'closed': Fore.CYAN,
3006 'error': Fore.WHITE,
3007 }.get(status, Fore.WHITE)
3008
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003009
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003010def get_cl_statuses(changes, fine_grained, max_processes=None):
3011 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003012
3013 If fine_grained is true, this will fetch CL statuses from the server.
3014 Otherwise, simply indicate if there's a matching url for the given branches.
3015
3016 If max_processes is specified, it is used as the maximum number of processes
3017 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3018 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003019
3020 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003021 """
3022 # Silence upload.py otherwise it becomes unwieldly.
3023 upload.verbosity = 0
3024
3025 if fine_grained:
3026 # Process one branch synchronously to work through authentication, then
3027 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003028 if changes:
3029 fetch = lambda cl: (cl, cl.GetStatus())
3030 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003031
kmarshall3bff56b2016-06-06 18:31:47 -07003032 if not changes:
3033 # Exit early if there was only one branch to fetch.
3034 return
3035
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003036 changes_to_fetch = changes[1:]
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003037 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003038 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003039 if max_processes is not None
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003040 else len(changes_to_fetch))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003041
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003042 fetched_cls = set()
3043 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003044 while True:
3045 try:
3046 row = it.next(timeout=5)
3047 except multiprocessing.TimeoutError:
3048 break
3049
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003050 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003051 yield row
3052
3053 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003054 for cl in set(changes_to_fetch) - fetched_cls:
3055 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003056
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003057 else:
3058 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003059 for cl in changes:
3060 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003061
rmistry@google.com2dd99862015-06-22 12:22:18 +00003062
3063def upload_branch_deps(cl, args):
3064 """Uploads CLs of local branches that are dependents of the current branch.
3065
3066 If the local branch dependency tree looks like:
3067 test1 -> test2.1 -> test3.1
3068 -> test3.2
3069 -> test2.2 -> test3.3
3070
3071 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3072 run on the dependent branches in this order:
3073 test2.1, test3.1, test3.2, test2.2, test3.3
3074
3075 Note: This function does not rebase your local dependent branches. Use it when
3076 you make a change to the parent branch that will not conflict with its
3077 dependent branches, and you would like their dependencies updated in
3078 Rietveld.
3079 """
3080 if git_common.is_dirty_git_tree('upload-branch-deps'):
3081 return 1
3082
3083 root_branch = cl.GetBranch()
3084 if root_branch is None:
3085 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3086 'Get on a branch!')
3087 if not cl.GetIssue() or not cl.GetPatchset():
3088 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3089 'patchset dependencies without an uploaded CL.')
3090
3091 branches = RunGit(['for-each-ref',
3092 '--format=%(refname:short) %(upstream:short)',
3093 'refs/heads'])
3094 if not branches:
3095 print('No local branches found.')
3096 return 0
3097
3098 # Create a dictionary of all local branches to the branches that are dependent
3099 # on it.
3100 tracked_to_dependents = collections.defaultdict(list)
3101 for b in branches.splitlines():
3102 tokens = b.split()
3103 if len(tokens) == 2:
3104 branch_name, tracked = tokens
3105 tracked_to_dependents[tracked].append(branch_name)
3106
vapiera7fbd5a2016-06-16 09:17:49 -07003107 print()
3108 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003109 dependents = []
3110 def traverse_dependents_preorder(branch, padding=''):
3111 dependents_to_process = tracked_to_dependents.get(branch, [])
3112 padding += ' '
3113 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003114 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003115 dependents.append(dependent)
3116 traverse_dependents_preorder(dependent, padding)
3117 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003118 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003119
3120 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003121 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003122 return 0
3123
vapiera7fbd5a2016-06-16 09:17:49 -07003124 print('This command will checkout all dependent branches and run '
3125 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003126 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3127
andybons@chromium.org962f9462016-02-03 20:00:42 +00003128 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003129 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003130 args.extend(['-t', 'Updated patchset dependency'])
3131
rmistry@google.com2dd99862015-06-22 12:22:18 +00003132 # Record all dependents that failed to upload.
3133 failures = {}
3134 # Go through all dependents, checkout the branch and upload.
3135 try:
3136 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003137 print()
3138 print('--------------------------------------')
3139 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003140 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003141 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003142 try:
3143 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003144 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003145 failures[dependent_branch] = 1
3146 except: # pylint: disable=W0702
3147 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003148 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003149 finally:
3150 # Swap back to the original root branch.
3151 RunGit(['checkout', '-q', root_branch])
3152
vapiera7fbd5a2016-06-16 09:17:49 -07003153 print()
3154 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003155 for dependent_branch in dependents:
3156 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003157 print(' %s : %s' % (dependent_branch, upload_status))
3158 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003159
3160 return 0
3161
3162
kmarshall3bff56b2016-06-06 18:31:47 -07003163def CMDarchive(parser, args):
3164 """Archives and deletes branches associated with closed changelists."""
3165 parser.add_option(
3166 '-j', '--maxjobs', action='store', type=int,
3167 help='The maximum number of jobs to use when retrieving review status')
3168 parser.add_option(
3169 '-f', '--force', action='store_true',
3170 help='Bypasses the confirmation prompt.')
3171
3172 auth.add_auth_options(parser)
3173 options, args = parser.parse_args(args)
3174 if args:
3175 parser.error('Unsupported args: %s' % ' '.join(args))
3176 auth_config = auth.extract_auth_config_from_options(options)
3177
3178 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3179 if not branches:
3180 return 0
3181
vapiera7fbd5a2016-06-16 09:17:49 -07003182 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003183 changes = [Changelist(branchref=b, auth_config=auth_config)
3184 for b in branches.splitlines()]
3185 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3186 statuses = get_cl_statuses(changes,
3187 fine_grained=True,
3188 max_processes=options.maxjobs)
3189 proposal = [(cl.GetBranch(),
3190 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3191 for cl, status in statuses
3192 if status == 'closed']
3193 proposal.sort()
3194
3195 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003196 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003197 return 0
3198
3199 current_branch = GetCurrentBranch()
3200
vapiera7fbd5a2016-06-16 09:17:49 -07003201 print('\nBranches with closed issues that will be archived:\n')
3202 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
kmarshall3bff56b2016-06-06 18:31:47 -07003203 for next_item in proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003204 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003205
3206 if any(branch == current_branch for branch, _ in proposal):
3207 print('You are currently on a branch \'%s\' which is associated with a '
3208 'closed codereview issue, so archive cannot proceed. Please '
3209 'checkout another branch and run this command again.' %
3210 current_branch)
3211 return 1
3212
3213 if not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003214 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3215 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003216 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003217 return 1
3218
3219 for branch, tagname in proposal:
3220 RunGit(['tag', tagname, branch])
3221 RunGit(['branch', '-D', branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003222 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003223
3224 return 0
3225
3226
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003227def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003228 """Show status of changelists.
3229
3230 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003231 - Red not sent for review or broken
3232 - Blue waiting for review
3233 - Yellow waiting for you to reply to review
3234 - Green LGTM'ed
3235 - Magenta in the commit queue
3236 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003237
3238 Also see 'git cl comments'.
3239 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003240 parser.add_option('--field',
3241 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003242 parser.add_option('-f', '--fast', action='store_true',
3243 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003244 parser.add_option(
3245 '-j', '--maxjobs', action='store', type=int,
3246 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003247
3248 auth.add_auth_options(parser)
3249 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003250 if args:
3251 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003252 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003253
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003254 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003255 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003256 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003257 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003258 elif options.field == 'id':
3259 issueid = cl.GetIssue()
3260 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003261 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003262 elif options.field == 'patch':
3263 patchset = cl.GetPatchset()
3264 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003265 print(patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003266 elif options.field == 'url':
3267 url = cl.GetIssueURL()
3268 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003269 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003270 return 0
3271
3272 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3273 if not branches:
3274 print('No local branch found.')
3275 return 0
3276
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003277 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003278 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003279 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003280 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003281 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003282 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003283 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003284
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003285 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003286 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3287 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3288 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003289 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003290 c, status = output.next()
3291 branch_statuses[c.GetBranch()] = status
3292 status = branch_statuses.pop(branch)
3293 url = cl.GetIssueURL()
3294 if url and (not status or status == 'error'):
3295 # The issue probably doesn't exist anymore.
3296 url += ' (broken)'
3297
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003298 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003299 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003300 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003301 color = ''
3302 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003303 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003304 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003305 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003306 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003307
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003308 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003309 print()
3310 print('Current branch:',)
3311 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003312 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003313 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003314 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003315 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003316 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003317 print('Issue description:')
3318 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003319 return 0
3320
3321
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003322def colorize_CMDstatus_doc():
3323 """To be called once in main() to add colors to git cl status help."""
3324 colors = [i for i in dir(Fore) if i[0].isupper()]
3325
3326 def colorize_line(line):
3327 for color in colors:
3328 if color in line.upper():
3329 # Extract whitespaces first and the leading '-'.
3330 indent = len(line) - len(line.lstrip(' ')) + 1
3331 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3332 return line
3333
3334 lines = CMDstatus.__doc__.splitlines()
3335 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3336
3337
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003338@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003339def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003340 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003341
3342 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003343 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003344 parser.add_option('-r', '--reverse', action='store_true',
3345 help='Lookup the branch(es) for the specified issues. If '
3346 'no issues are specified, all branches with mapped '
3347 'issues will be listed.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003348 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003349 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003350 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003351
dnj@chromium.org406c4402015-03-03 17:22:28 +00003352 if options.reverse:
3353 branches = RunGit(['for-each-ref', 'refs/heads',
3354 '--format=%(refname:short)']).splitlines()
3355
3356 # Reverse issue lookup.
3357 issue_branch_map = {}
3358 for branch in branches:
3359 cl = Changelist(branchref=branch)
3360 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3361 if not args:
3362 args = sorted(issue_branch_map.iterkeys())
3363 for issue in args:
3364 if not issue:
3365 continue
vapiera7fbd5a2016-06-16 09:17:49 -07003366 print('Branch for issue number %s: %s' % (
3367 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
dnj@chromium.org406c4402015-03-03 17:22:28 +00003368 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003369 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003370 if len(args) > 0:
3371 try:
3372 issue = int(args[0])
3373 except ValueError:
3374 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003375 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003376 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003377 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003378 return 0
3379
3380
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003381def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003382 """Shows or posts review comments for any changelist."""
3383 parser.add_option('-a', '--add-comment', dest='comment',
3384 help='comment to add to an issue')
3385 parser.add_option('-i', dest='issue',
3386 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003387 parser.add_option('-j', '--json-file',
3388 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003389 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003390 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003391 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003392
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003393 issue = None
3394 if options.issue:
3395 try:
3396 issue = int(options.issue)
3397 except ValueError:
3398 DieWithError('A review issue id is expected to be a number')
3399
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003400 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003401
3402 if options.comment:
3403 cl.AddComment(options.comment)
3404 return 0
3405
3406 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003407 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003408 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003409 summary.append({
3410 'date': message['date'],
3411 'lgtm': False,
3412 'message': message['text'],
3413 'not_lgtm': False,
3414 'sender': message['sender'],
3415 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003416 if message['disapproval']:
3417 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003418 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003419 elif message['approval']:
3420 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003421 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003422 elif message['sender'] == data['owner_email']:
3423 color = Fore.MAGENTA
3424 else:
3425 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003426 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003427 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003428 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003429 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003430 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003431 if options.json_file:
3432 with open(options.json_file, 'wb') as f:
3433 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003434 return 0
3435
3436
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003437@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003438def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003439 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003440 parser.add_option('-d', '--display', action='store_true',
3441 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003442 parser.add_option('-n', '--new-description',
3443 help='New description to set for this issue (- for stdin)')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003444
3445 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003446 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003447 options, args = parser.parse_args(args)
3448 _process_codereview_select_options(parser, options)
3449
3450 target_issue = None
3451 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07003452 target_issue = ParseIssueNumberArgument(args[0])
3453 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003454 parser.print_help()
3455 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003456
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003457 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003458
martiniss6eda05f2016-06-30 10:18:35 -07003459 kwargs = {
3460 'auth_config': auth_config,
3461 'codereview': options.forced_codereview,
3462 }
3463 if target_issue:
3464 kwargs['issue'] = target_issue.issue
3465 if options.forced_codereview == 'rietveld':
3466 kwargs['rietveld_server'] = target_issue.hostname
3467
3468 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003469
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003470 if not cl.GetIssue():
3471 DieWithError('This branch has no associated changelist.')
3472 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003473
smut@google.com34fb6b12015-07-13 20:03:26 +00003474 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003475 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003476 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003477
3478 if options.new_description:
3479 text = options.new_description
3480 if text == '-':
3481 text = '\n'.join(l.rstrip() for l in sys.stdin)
3482
3483 description.set_description(text)
3484 else:
3485 description.prompt()
3486
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003487 if cl.GetDescription() != description.description:
3488 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003489 return 0
3490
3491
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003492def CreateDescriptionFromLog(args):
3493 """Pulls out the commit log to use as a base for the CL description."""
3494 log_args = []
3495 if len(args) == 1 and not args[0].endswith('.'):
3496 log_args = [args[0] + '..']
3497 elif len(args) == 1 and args[0].endswith('...'):
3498 log_args = [args[0][:-1]]
3499 elif len(args) == 2:
3500 log_args = [args[0] + '..' + args[1]]
3501 else:
3502 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003503 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003504
3505
thestig@chromium.org44202a22014-03-11 19:22:18 +00003506def CMDlint(parser, args):
3507 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003508 parser.add_option('--filter', action='append', metavar='-x,+y',
3509 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003510 auth.add_auth_options(parser)
3511 options, args = parser.parse_args(args)
3512 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003513
3514 # Access to a protected member _XX of a client class
3515 # pylint: disable=W0212
3516 try:
3517 import cpplint
3518 import cpplint_chromium
3519 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003520 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003521 return 1
3522
3523 # Change the current working directory before calling lint so that it
3524 # shows the correct base.
3525 previous_cwd = os.getcwd()
3526 os.chdir(settings.GetRoot())
3527 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003528 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003529 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3530 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003531 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003532 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003533 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003534
3535 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003536 command = args + files
3537 if options.filter:
3538 command = ['--filter=' + ','.join(options.filter)] + command
3539 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003540
3541 white_regex = re.compile(settings.GetLintRegex())
3542 black_regex = re.compile(settings.GetLintIgnoreRegex())
3543 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3544 for filename in filenames:
3545 if white_regex.match(filename):
3546 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07003547 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003548 else:
3549 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3550 extra_check_functions)
3551 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003552 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003553 finally:
3554 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07003555 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003556 if cpplint._cpplint_state.error_count != 0:
3557 return 1
3558 return 0
3559
3560
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003561def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003562 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003563 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003564 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003565 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003566 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003567 auth.add_auth_options(parser)
3568 options, args = parser.parse_args(args)
3569 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003570
sbc@chromium.org71437c02015-04-09 19:29:40 +00003571 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07003572 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003573 return 1
3574
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003575 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003576 if args:
3577 base_branch = args[0]
3578 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003579 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003580 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003581
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003582 cl.RunHook(
3583 committing=not options.upload,
3584 may_prompt=False,
3585 verbose=options.verbose,
3586 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003587 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003588
3589
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003590def GenerateGerritChangeId(message):
3591 """Returns Ixxxxxx...xxx change id.
3592
3593 Works the same way as
3594 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3595 but can be called on demand on all platforms.
3596
3597 The basic idea is to generate git hash of a state of the tree, original commit
3598 message, author/committer info and timestamps.
3599 """
3600 lines = []
3601 tree_hash = RunGitSilent(['write-tree'])
3602 lines.append('tree %s' % tree_hash.strip())
3603 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3604 if code == 0:
3605 lines.append('parent %s' % parent.strip())
3606 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3607 lines.append('author %s' % author.strip())
3608 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3609 lines.append('committer %s' % committer.strip())
3610 lines.append('')
3611 # Note: Gerrit's commit-hook actually cleans message of some lines and
3612 # whitespace. This code is not doing this, but it clearly won't decrease
3613 # entropy.
3614 lines.append(message)
3615 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3616 stdin='\n'.join(lines))
3617 return 'I%s' % change_hash.strip()
3618
3619
wittman@chromium.org455dc922015-01-26 20:15:50 +00003620def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3621 """Computes the remote branch ref to use for the CL.
3622
3623 Args:
3624 remote (str): The git remote for the CL.
3625 remote_branch (str): The git remote branch for the CL.
3626 target_branch (str): The target branch specified by the user.
3627 pending_prefix (str): The pending prefix from the settings.
3628 """
3629 if not (remote and remote_branch):
3630 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003631
wittman@chromium.org455dc922015-01-26 20:15:50 +00003632 if target_branch:
3633 # Cannonicalize branch references to the equivalent local full symbolic
3634 # refs, which are then translated into the remote full symbolic refs
3635 # below.
3636 if '/' not in target_branch:
3637 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3638 else:
3639 prefix_replacements = (
3640 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3641 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3642 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3643 )
3644 match = None
3645 for regex, replacement in prefix_replacements:
3646 match = re.search(regex, target_branch)
3647 if match:
3648 remote_branch = target_branch.replace(match.group(0), replacement)
3649 break
3650 if not match:
3651 # This is a branch path but not one we recognize; use as-is.
3652 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003653 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3654 # Handle the refs that need to land in different refs.
3655 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003656
wittman@chromium.org455dc922015-01-26 20:15:50 +00003657 # Create the true path to the remote branch.
3658 # Does the following translation:
3659 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3660 # * refs/remotes/origin/master -> refs/heads/master
3661 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3662 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3663 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3664 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3665 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3666 'refs/heads/')
3667 elif remote_branch.startswith('refs/remotes/branch-heads'):
3668 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3669 # If a pending prefix exists then replace refs/ with it.
3670 if pending_prefix:
3671 remote_branch = remote_branch.replace('refs/', pending_prefix)
3672 return remote_branch
3673
3674
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003675def cleanup_list(l):
3676 """Fixes a list so that comma separated items are put as individual items.
3677
3678 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3679 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3680 """
3681 items = sum((i.split(',') for i in l), [])
3682 stripped_items = (i.strip() for i in items)
3683 return sorted(filter(None, stripped_items))
3684
3685
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003686@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003687def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003688 """Uploads the current changelist to codereview.
3689
3690 Can skip dependency patchset uploads for a branch by running:
3691 git config branch.branch_name.skip-deps-uploads True
3692 To unset run:
3693 git config --unset branch.branch_name.skip-deps-uploads
3694 Can also set the above globally by using the --global flag.
3695 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003696 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3697 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003698 parser.add_option('--bypass-watchlists', action='store_true',
3699 dest='bypass_watchlists',
3700 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003701 parser.add_option('-f', action='store_true', dest='force',
3702 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003703 parser.add_option('-m', dest='message', help='message for patchset')
tandriib80458a2016-06-23 12:20:07 -07003704 parser.add_option('--message-file', dest='message_file',
3705 help='file which contains message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003706 parser.add_option('-t', dest='title',
3707 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003708 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003709 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003710 help='reviewer email addresses')
3711 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003712 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003713 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003714 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003715 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003716 parser.add_option('--emulate_svn_auto_props',
3717 '--emulate-svn-auto-props',
3718 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003719 dest="emulate_svn_auto_props",
3720 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003721 parser.add_option('-c', '--use-commit-queue', action='store_true',
3722 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003723 parser.add_option('--private', action='store_true',
3724 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003725 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003726 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003727 metavar='TARGET',
3728 help='Apply CL to remote ref TARGET. ' +
3729 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003730 parser.add_option('--squash', action='store_true',
3731 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003732 parser.add_option('--no-squash', action='store_true',
3733 help='Don\'t squash multiple commits into one ' +
3734 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003735 parser.add_option('--email', default=None,
3736 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003737 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3738 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003739 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3740 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003741 help='Send the patchset to do a CQ dry run right after '
3742 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003743 parser.add_option('--dependencies', action='store_true',
3744 help='Uploads CLs of all the local branches that depend on '
3745 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003746
rmistry@google.com2dd99862015-06-22 12:22:18 +00003747 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003748 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003749 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003750 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003751 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003752 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003753 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003754
sbc@chromium.org71437c02015-04-09 19:29:40 +00003755 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003756 return 1
3757
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003758 options.reviewers = cleanup_list(options.reviewers)
3759 options.cc = cleanup_list(options.cc)
3760
tandriib80458a2016-06-23 12:20:07 -07003761 if options.message_file:
3762 if options.message:
3763 parser.error('only one of --message and --message-file allowed.')
3764 options.message = gclient_utils.FileRead(options.message_file)
3765 options.message_file = None
3766
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003767 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3768 settings.GetIsGerrit()
3769
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003770 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00003771 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003772
3773
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003774def IsSubmoduleMergeCommit(ref):
3775 # When submodules are added to the repo, we expect there to be a single
3776 # non-git-svn merge commit at remote HEAD with a signature comment.
3777 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003778 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003779 return RunGit(cmd) != ''
3780
3781
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003782def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003783 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003784
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003785 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3786 upstream and closes the issue automatically and atomically.
3787
3788 Otherwise (in case of Rietveld):
3789 Squashes branch into a single commit.
3790 Updates changelog with metadata (e.g. pointer to review).
3791 Pushes/dcommits the code upstream.
3792 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003793 """
3794 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3795 help='bypass upload presubmit hook')
3796 parser.add_option('-m', dest='message',
3797 help="override review description")
3798 parser.add_option('-f', action='store_true', dest='force',
3799 help="force yes to questions (don't prompt)")
3800 parser.add_option('-c', dest='contributor',
3801 help="external contributor for patch (appended to " +
3802 "description and used as author for git). Should be " +
3803 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003804 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003805 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003806 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003807 auth_config = auth.extract_auth_config_from_options(options)
3808
3809 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003810
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003811 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3812 if cl.IsGerrit():
3813 if options.message:
3814 # This could be implemented, but it requires sending a new patch to
3815 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3816 # Besides, Gerrit has the ability to change the commit message on submit
3817 # automatically, thus there is no need to support this option (so far?).
3818 parser.error('-m MESSAGE option is not supported for Gerrit.')
3819 if options.contributor:
3820 parser.error(
3821 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3822 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3823 'the contributor\'s "name <email>". If you can\'t upload such a '
3824 'commit for review, contact your repository admin and request'
3825 '"Forge-Author" permission.')
3826 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3827 options.verbose)
3828
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003829 current = cl.GetBranch()
3830 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3831 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07003832 print()
3833 print('Attempting to push branch %r into another local branch!' % current)
3834 print()
3835 print('Either reparent this branch on top of origin/master:')
3836 print(' git reparent-branch --root')
3837 print()
3838 print('OR run `git rebase-update` if you think the parent branch is ')
3839 print('already committed.')
3840 print()
3841 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003842 return 1
3843
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003844 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003845 # Default to merging against our best guess of the upstream branch.
3846 args = [cl.GetUpstreamBranch()]
3847
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003848 if options.contributor:
3849 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07003850 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003851 return 1
3852
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003853 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003854 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003855
sbc@chromium.org71437c02015-04-09 19:29:40 +00003856 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003857 return 1
3858
3859 # This rev-list syntax means "show all commits not in my branch that
3860 # are in base_branch".
3861 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3862 base_branch]).splitlines()
3863 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07003864 print('Base branch "%s" has %d commits '
3865 'not in this branch.' % (base_branch, len(upstream_commits)))
3866 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003867 return 1
3868
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003869 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003870 svn_head = None
3871 if cmd == 'dcommit' or base_has_submodules:
3872 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
3873 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003874
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003875 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003876 # If the base_head is a submodule merge commit, the first parent of the
3877 # base_head should be a git-svn commit, which is what we're interested in.
3878 base_svn_head = base_branch
3879 if base_has_submodules:
3880 base_svn_head += '^1'
3881
3882 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003883 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07003884 print('This branch has %d additional commits not upstreamed yet.'
3885 % len(extra_commits.splitlines()))
3886 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
3887 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003888 return 1
3889
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003890 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003891 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003892 author = None
3893 if options.contributor:
3894 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003895 hook_results = cl.RunHook(
3896 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003897 may_prompt=not options.force,
3898 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003899 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003900 if not hook_results.should_continue():
3901 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003902
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003903 # Check the tree status if the tree status URL is set.
3904 status = GetTreeStatus()
3905 if 'closed' == status:
3906 print('The tree is closed. Please wait for it to reopen. Use '
3907 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3908 return 1
3909 elif 'unknown' == status:
3910 print('Unable to determine tree status. Please verify manually and '
3911 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3912 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003913
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003914 change_desc = ChangeDescription(options.message)
3915 if not change_desc.description and cl.GetIssue():
3916 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003917
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003918 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00003919 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003920 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00003921 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003922 print('No description set.')
3923 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00003924 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003925
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003926 # Keep a separate copy for the commit message, because the commit message
3927 # contains the link to the Rietveld issue, while the Rietveld message contains
3928 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003929 # Keep a separate copy for the commit message.
3930 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00003931 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003932
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003933 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00003934 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00003935 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00003936 # after it. Add a period on a new line to circumvent this. Also add a space
3937 # before the period to make sure that Gitiles continues to correctly resolve
3938 # the URL.
3939 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003940 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003941 commit_desc.append_footer('Patch from %s.' % options.contributor)
3942
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00003943 print('Description:')
3944 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003945
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003946 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003947 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00003948 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003949
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003950 # We want to squash all this branch's commits into one commit with the proper
3951 # description. We do this by doing a "reset --soft" to the base branch (which
3952 # keeps the working copy the same), then dcommitting that. If origin/master
3953 # has a submodule merge commit, we'll also need to cherry-pick the squashed
3954 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003955 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003956 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
3957 # Delete the branches if they exist.
3958 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
3959 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
3960 result = RunGitWithCode(showref_cmd)
3961 if result[0] == 0:
3962 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003963
3964 # We might be in a directory that's present in this branch but not in the
3965 # trunk. Move up to the top of the tree so that git commands that expect a
3966 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003967 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003968 if rel_base_path:
3969 os.chdir(rel_base_path)
3970
3971 # Stuff our change into the merge branch.
3972 # We wrap in a try...finally block so if anything goes wrong,
3973 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003974 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003975 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003976 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003977 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003978 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00003979 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003980 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003981 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003982 RunGit(
3983 [
3984 'commit', '--author', options.contributor,
3985 '-m', commit_desc.description,
3986 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003987 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003988 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003989 if base_has_submodules:
3990 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
3991 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
3992 RunGit(['checkout', CHERRY_PICK_BRANCH])
3993 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003994 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003995 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003996 mirror = settings.GetGitMirror(remote)
3997 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003998 pending_prefix = settings.GetPendingRefPrefix()
3999 if not pending_prefix or branch.startswith(pending_prefix):
4000 # If not using refs/pending/heads/* at all, or target ref is already set
4001 # to pending, then push to the target ref directly.
4002 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004003 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004004 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004005 else:
4006 # Cherry-pick the change on top of pending ref and then push it.
4007 assert branch.startswith('refs/'), branch
4008 assert pending_prefix[-1] == '/', pending_prefix
4009 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004010 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004011 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004012 if retcode == 0:
4013 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004014 else:
4015 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004016 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00004017 'svn', 'dcommit',
4018 '-C%s' % options.similarity,
4019 '--no-rebase', '--rmdir',
4020 ]
4021 if settings.GetForceHttpsCommitUrl():
4022 # Allow forcing https commit URLs for some projects that don't allow
4023 # committing to http URLs (like Google Code).
4024 remote_url = cl.GetGitSvnRemoteUrl()
4025 if urlparse.urlparse(remote_url).scheme == 'http':
4026 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004027 cmd_args.append('--commit-url=%s' % remote_url)
4028 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004029 if 'Committed r' in output:
4030 revision = re.match(
4031 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4032 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004033 finally:
4034 # And then swap back to the original branch and clean up.
4035 RunGit(['checkout', '-q', cl.GetBranch()])
4036 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004037 if base_has_submodules:
4038 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004039
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004040 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004041 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004042 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004043
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004044 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004045 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004046 try:
4047 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4048 # We set pushed_to_pending to False, since it made it all the way to the
4049 # real ref.
4050 pushed_to_pending = False
4051 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004052 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004053
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004054 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004055 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004056 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004057 if not to_pending:
4058 if viewvc_url and revision:
4059 change_desc.append_footer(
4060 'Committed: %s%s' % (viewvc_url, revision))
4061 elif revision:
4062 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004063 print('Closing issue '
4064 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004065 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004066 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004067 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004068 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004069 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004070 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004071 if options.bypass_hooks:
4072 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4073 else:
4074 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004075 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004076
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004077 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004078 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004079 print('The commit is in the pending queue (%s).' % pending_ref)
4080 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4081 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004082
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004083 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4084 if os.path.isfile(hook):
4085 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004086
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004087 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004088
4089
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004090def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004091 print()
4092 print('Waiting for commit to be landed on %s...' % real_ref)
4093 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004094 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4095 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004096 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004097
4098 loop = 0
4099 while True:
4100 sys.stdout.write('fetching (%d)... \r' % loop)
4101 sys.stdout.flush()
4102 loop += 1
4103
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004104 if mirror:
4105 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004106 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4107 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4108 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4109 for commit in commits.splitlines():
4110 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004111 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004112 return commit
4113
4114 current_rev = to_rev
4115
4116
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004117def PushToGitPending(remote, pending_ref, upstream_ref):
4118 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4119
4120 Returns:
4121 (retcode of last operation, output log of last operation).
4122 """
4123 assert pending_ref.startswith('refs/'), pending_ref
4124 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4125 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4126 code = 0
4127 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004128 max_attempts = 3
4129 attempts_left = max_attempts
4130 while attempts_left:
4131 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004132 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004133 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004134
4135 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004136 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004137 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004138 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004139 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004140 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004141 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004142 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004143 continue
4144
4145 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004146 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004147 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004148 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004149 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004150 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4151 'the following files have merge conflicts:' % pending_ref)
4152 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4153 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004154 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004155 return code, out
4156
4157 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004158 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004159 code, out = RunGitWithCode(
4160 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4161 if code == 0:
4162 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004163 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004164 return code, out
4165
vapiera7fbd5a2016-06-16 09:17:49 -07004166 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004167 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004168 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004169 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004170 print('Fatal push error. Make sure your .netrc credentials and git '
4171 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004172 return code, out
4173
vapiera7fbd5a2016-06-16 09:17:49 -07004174 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004175 return code, out
4176
4177
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004178def IsFatalPushFailure(push_stdout):
4179 """True if retrying push won't help."""
4180 return '(prohibited by Gerrit)' in push_stdout
4181
4182
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004183@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004184def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004185 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004186 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004187 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004188 # If it looks like previous commits were mirrored with git-svn.
4189 message = """This repository appears to be a git-svn mirror, but no
4190upstream SVN master is set. You probably need to run 'git auto-svn' once."""
4191 else:
4192 message = """This doesn't appear to be an SVN repository.
4193If your project has a true, writeable git repository, you probably want to run
4194'git cl land' instead.
4195If your project has a git mirror of an upstream SVN master, you probably need
4196to run 'git svn init'.
4197
4198Using the wrong command might cause your commit to appear to succeed, and the
4199review to be closed, without actually landing upstream. If you choose to
4200proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004201 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004202 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
tandrii3bb82ff2016-06-17 07:36:36 -07004203 # TODO(tandrii): kill this post SVN migration with
4204 # https://codereview.chromium.org/2076683002
4205 print('WARNING: chrome infrastructure is migrating SVN repos to Git.\n'
4206 'Please let us know of this project you are committing to:'
4207 ' http://crbug.com/600451')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004208 return SendUpstream(parser, args, 'dcommit')
4209
4210
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004211@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004212def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004213 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004214 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004215 print('This appears to be an SVN repository.')
4216 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004217 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004218 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004219 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004220
4221
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004222@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004223def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004224 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004225 parser.add_option('-b', dest='newbranch',
4226 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004227 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004228 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004229 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4230 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004231 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004232 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004233 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004234 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004235 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004236 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004237
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004238
4239 group = optparse.OptionGroup(
4240 parser,
4241 'Options for continuing work on the current issue uploaded from a '
4242 'different clone (e.g. different machine). Must be used independently '
4243 'from the other options. No issue number should be specified, and the '
4244 'branch must have an issue number associated with it')
4245 group.add_option('--reapply', action='store_true', dest='reapply',
4246 help='Reset the branch and reapply the issue.\n'
4247 'CAUTION: This will undo any local changes in this '
4248 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004249
4250 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004251 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004252 parser.add_option_group(group)
4253
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004254 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004255 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004256 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004257 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004258 auth_config = auth.extract_auth_config_from_options(options)
4259
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004260
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004261 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004262 if options.newbranch:
4263 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004264 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004265 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004266
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004267 cl = Changelist(auth_config=auth_config,
4268 codereview=options.forced_codereview)
4269 if not cl.GetIssue():
4270 parser.error('current branch must have an associated issue')
4271
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004272 upstream = cl.GetUpstreamBranch()
4273 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004274 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004275
4276 RunGit(['reset', '--hard', upstream])
4277 if options.pull:
4278 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004279
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004280 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4281 options.directory)
4282
4283 if len(args) != 1 or not args[0]:
4284 parser.error('Must specify issue number or url')
4285
4286 # We don't want uncommitted changes mixed up with the patch.
4287 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004288 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004289
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004290 if options.newbranch:
4291 if options.force:
4292 RunGit(['branch', '-D', options.newbranch],
4293 stderr=subprocess2.PIPE, error_ok=True)
4294 RunGit(['new-branch', options.newbranch])
4295
4296 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4297
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004298 if cl.IsGerrit():
4299 if options.reject:
4300 parser.error('--reject is not supported with Gerrit codereview.')
4301 if options.nocommit:
4302 parser.error('--nocommit is not supported with Gerrit codereview.')
4303 if options.directory:
4304 parser.error('--directory is not supported with Gerrit codereview.')
4305
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004306 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004307 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004308
4309
4310def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004311 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004312 # Provide a wrapper for git svn rebase to help avoid accidental
4313 # git svn dcommit.
4314 # It's the only command that doesn't use parser at all since we just defer
4315 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004316
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004317 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004318
4319
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004320def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004321 """Fetches the tree status and returns either 'open', 'closed',
4322 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004323 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004324 if url:
4325 status = urllib2.urlopen(url).read().lower()
4326 if status.find('closed') != -1 or status == '0':
4327 return 'closed'
4328 elif status.find('open') != -1 or status == '1':
4329 return 'open'
4330 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004331 return 'unset'
4332
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004333
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004334def GetTreeStatusReason():
4335 """Fetches the tree status from a json url and returns the message
4336 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004337 url = settings.GetTreeStatusUrl()
4338 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004339 connection = urllib2.urlopen(json_url)
4340 status = json.loads(connection.read())
4341 connection.close()
4342 return status['message']
4343
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004344
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004345def GetBuilderMaster(bot_list):
4346 """For a given builder, fetch the master from AE if available."""
4347 map_url = 'https://builders-map.appspot.com/'
4348 try:
4349 master_map = json.load(urllib2.urlopen(map_url))
4350 except urllib2.URLError as e:
4351 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4352 (map_url, e))
4353 except ValueError as e:
4354 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4355 if not master_map:
4356 return None, 'Failed to build master map.'
4357
4358 result_master = ''
4359 for bot in bot_list:
4360 builder = bot.split(':', 1)[0]
4361 master_list = master_map.get(builder, [])
4362 if not master_list:
4363 return None, ('No matching master for builder %s.' % builder)
4364 elif len(master_list) > 1:
4365 return None, ('The builder name %s exists in multiple masters %s.' %
4366 (builder, master_list))
4367 else:
4368 cur_master = master_list[0]
4369 if not result_master:
4370 result_master = cur_master
4371 elif result_master != cur_master:
4372 return None, 'The builders do not belong to the same master.'
4373 return result_master, None
4374
4375
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004376def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004377 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004378 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004379 status = GetTreeStatus()
4380 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004381 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004382 return 2
4383
vapiera7fbd5a2016-06-16 09:17:49 -07004384 print('The tree is %s' % status)
4385 print()
4386 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004387 if status != 'open':
4388 return 1
4389 return 0
4390
4391
maruel@chromium.org15192402012-09-06 12:38:29 +00004392def CMDtry(parser, args):
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004393 """Triggers try jobs through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00004394 group = optparse.OptionGroup(parser, "Try job options")
4395 group.add_option(
4396 "-b", "--bot", action="append",
4397 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
4398 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004399 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00004400 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004401 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00004402 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004403 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00004404 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004405 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004406 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004407 "-r", "--revision",
4408 help="Revision to use for the try job; default: the "
4409 "revision will be determined by the try server; see "
4410 "its waterfall for more info")
4411 group.add_option(
4412 "-c", "--clobber", action="store_true", default=False,
4413 help="Force a clobber before building; e.g. don't do an "
4414 "incremental build")
4415 group.add_option(
4416 "--project",
4417 help="Override which project to use. Projects are defined "
4418 "server-side to define what default bot set to use")
4419 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004420 "-p", "--property", dest="properties", action="append", default=[],
4421 help="Specify generic properties in the form -p key1=value1 -p "
4422 "key2=value2 etc (buildbucket only). The value will be treated as "
4423 "json if decodable, or as string otherwise.")
4424 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004425 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004426 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004427 "--use-rietveld", action="store_true", default=False,
4428 help="Use Rietveld to trigger try jobs.")
4429 group.add_option(
4430 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4431 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004432 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004433 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004434 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004435 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004436
machenbach@chromium.org45453142015-09-15 08:45:22 +00004437 if options.use_rietveld and options.properties:
4438 parser.error('Properties can only be specified with buildbucket')
4439
4440 # Make sure that all properties are prop=value pairs.
4441 bad_params = [x for x in options.properties if '=' not in x]
4442 if bad_params:
4443 parser.error('Got properties with missing "=": %s' % bad_params)
4444
maruel@chromium.org15192402012-09-06 12:38:29 +00004445 if args:
4446 parser.error('Unknown arguments: %s' % args)
4447
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004448 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004449 if not cl.GetIssue():
4450 parser.error('Need to upload first')
4451
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004452 if cl.IsGerrit():
4453 parser.error(
4454 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4455 'If your project has Commit Queue, dry run is a workaround:\n'
4456 ' git cl set-commit --dry-run')
4457 # Code below assumes Rietveld issue.
4458 # TODO(tandrii): actually implement for Gerrit http://crbug.com/599931.
4459
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004460 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004461 if props.get('closed'):
4462 parser.error('Cannot send tryjobs for a closed CL')
4463
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004464 if props.get('private'):
4465 parser.error('Cannot use trybots with private issue')
4466
maruel@chromium.org15192402012-09-06 12:38:29 +00004467 if not options.name:
4468 options.name = cl.GetBranch()
4469
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004470 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004471 options.master, err_msg = GetBuilderMaster(options.bot)
4472 if err_msg:
4473 parser.error('Tryserver master cannot be found because: %s\n'
4474 'Please manually specify the tryserver master'
4475 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004476
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004477 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004478 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004479 if not options.bot:
4480 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004481
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004482 # Get try masters from PRESUBMIT.py files.
4483 masters = presubmit_support.DoGetTryMasters(
4484 change,
4485 change.LocalPaths(),
4486 settings.GetRoot(),
4487 None,
4488 None,
4489 options.verbose,
4490 sys.stdout)
4491 if masters:
4492 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004493
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004494 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4495 options.bot = presubmit_support.DoGetTrySlaves(
4496 change,
4497 change.LocalPaths(),
4498 settings.GetRoot(),
4499 None,
4500 None,
4501 options.verbose,
4502 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004503
4504 if not options.bot:
4505 # Get try masters from cq.cfg if any.
4506 # TODO(tandrii): some (but very few) projects store cq.cfg in different
4507 # location.
4508 cq_cfg = os.path.join(change.RepositoryRoot(),
4509 'infra', 'config', 'cq.cfg')
4510 if os.path.exists(cq_cfg):
4511 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00004512 cq_masters = commit_queue.get_master_builder_map(
4513 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004514 for master, builders in cq_masters.iteritems():
4515 for builder in builders:
4516 # Skip presubmit builders, because these will fail without LGTM.
machenbach@chromium.org2403e802016-04-29 12:34:42 +00004517 masters.setdefault(master, {})[builder] = ['defaulttests']
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004518 if masters:
tandriib93dd2b2016-06-07 08:03:08 -07004519 print('Loaded default bots from CQ config (%s)' % cq_cfg)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004520 return masters
tandriib93dd2b2016-06-07 08:03:08 -07004521 else:
4522 print('CQ config exists (%s) but has no try bots listed' % cq_cfg)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004523
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004524 if not options.bot:
4525 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00004526
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004527 builders_and_tests = {}
4528 # TODO(machenbach): The old style command-line options don't support
4529 # multiple try masters yet.
4530 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4531 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4532
4533 for bot in old_style:
4534 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004535 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004536 elif ',' in bot:
4537 parser.error('Specify one bot per --bot flag')
4538 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004539 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004540
4541 for bot, tests in new_style:
4542 builders_and_tests.setdefault(bot, []).extend(tests)
4543
4544 # Return a master map with one master to be backwards compatible. The
4545 # master name defaults to an empty string, which will cause the master
4546 # not to be set on rietveld (deprecated).
4547 return {options.master: builders_and_tests}
4548
4549 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004550
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004551 for builders in masters.itervalues():
4552 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004553 print('ERROR You are trying to send a job to a triggered bot. This type '
4554 'of bot requires an\ninitial job from a parent (usually a builder).'
4555 ' Instead send your job to the parent.\n'
4556 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004557 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004558
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004559 patchset = cl.GetMostRecentPatchset()
4560 if patchset and patchset != cl.GetPatchset():
4561 print(
4562 '\nWARNING Mismatch between local config and server. Did a previous '
4563 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4564 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004565 if options.luci:
4566 trigger_luci_job(cl, masters, options)
4567 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004568 try:
4569 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4570 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004571 print('ERROR: %s' % ex)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004572 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004573 except Exception as e:
4574 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
vapiera7fbd5a2016-06-16 09:17:49 -07004575 print('ERROR: Exception when trying to trigger tryjobs: %s\n%s' %
4576 (e, stacktrace))
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004577 return 1
4578 else:
4579 try:
4580 cl.RpcServer().trigger_distributed_try_jobs(
4581 cl.GetIssue(), patchset, options.name, options.clobber,
4582 options.revision, masters)
4583 except urllib2.HTTPError as e:
4584 if e.code == 404:
4585 print('404 from rietveld; '
4586 'did you mean to use "git try" instead of "git cl try"?')
4587 return 1
4588 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004589
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004590 for (master, builders) in sorted(masters.iteritems()):
4591 if master:
vapiera7fbd5a2016-06-16 09:17:49 -07004592 print('Master: %s' % master)
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004593 length = max(len(builder) for builder in builders)
4594 for builder in sorted(builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004595 print(' %*s: %s' % (length, builder, ','.join(builders[builder])))
maruel@chromium.org15192402012-09-06 12:38:29 +00004596 return 0
4597
4598
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004599def CMDtry_results(parser, args):
4600 group = optparse.OptionGroup(parser, "Try job results options")
4601 group.add_option(
4602 "-p", "--patchset", type=int, help="patchset number if not current.")
4603 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004604 "--print-master", action='store_true', help="print master name as well.")
4605 group.add_option(
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004606 "--color", action='store_true', default=setup_color.IS_TTY,
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004607 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004608 group.add_option(
4609 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4610 help="Host of buildbucket. The default host is %default.")
4611 parser.add_option_group(group)
4612 auth.add_auth_options(parser)
4613 options, args = parser.parse_args(args)
4614 if args:
4615 parser.error('Unrecognized args: %s' % ' '.join(args))
4616
4617 auth_config = auth.extract_auth_config_from_options(options)
4618 cl = Changelist(auth_config=auth_config)
4619 if not cl.GetIssue():
4620 parser.error('Need to upload first')
4621
4622 if not options.patchset:
4623 options.patchset = cl.GetMostRecentPatchset()
4624 if options.patchset and options.patchset != cl.GetPatchset():
4625 print(
4626 '\nWARNING Mismatch between local config and server. Did a previous '
4627 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4628 'Continuing using\npatchset %s.\n' % options.patchset)
4629 try:
4630 jobs = fetch_try_jobs(auth_config, cl, options)
4631 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004632 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004633 return 1
4634 except Exception as e:
4635 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
vapiera7fbd5a2016-06-16 09:17:49 -07004636 print('ERROR: Exception when trying to fetch tryjobs: %s\n%s' %
4637 (e, stacktrace))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004638 return 1
4639 print_tryjobs(options, jobs)
4640 return 0
4641
4642
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004643@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004644def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004645 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004646 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004647 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004648 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004649
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004650 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004651 if args:
4652 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004653 branch = cl.GetBranch()
4654 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004655 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004656 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004657
4658 # Clear configured merge-base, if there is one.
4659 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004660 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004661 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004662 return 0
4663
4664
thestig@chromium.org00858c82013-12-02 23:08:03 +00004665def CMDweb(parser, args):
4666 """Opens the current CL in the web browser."""
4667 _, args = parser.parse_args(args)
4668 if args:
4669 parser.error('Unrecognized args: %s' % ' '.join(args))
4670
4671 issue_url = Changelist().GetIssueURL()
4672 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004673 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004674 return 1
4675
4676 webbrowser.open(issue_url)
4677 return 0
4678
4679
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004680def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004681 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004682 parser.add_option('-d', '--dry-run', action='store_true',
4683 help='trigger in dry run mode')
4684 parser.add_option('-c', '--clear', action='store_true',
4685 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004686 auth.add_auth_options(parser)
4687 options, args = parser.parse_args(args)
4688 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004689 if args:
4690 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004691 if options.dry_run and options.clear:
4692 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4693
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004694 cl = Changelist(auth_config=auth_config)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004695 if options.clear:
4696 state = _CQState.CLEAR
4697 elif options.dry_run:
4698 state = _CQState.DRY_RUN
4699 else:
4700 state = _CQState.COMMIT
4701 if not cl.GetIssue():
4702 parser.error('Must upload the issue first')
4703 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004704 return 0
4705
4706
groby@chromium.org411034a2013-02-26 15:12:01 +00004707def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004708 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004709 auth.add_auth_options(parser)
4710 options, args = parser.parse_args(args)
4711 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004712 if args:
4713 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004714 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00004715 # Ensure there actually is an issue to close.
4716 cl.GetDescription()
4717 cl.CloseIssue()
4718 return 0
4719
4720
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004721def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004722 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004723 auth.add_auth_options(parser)
4724 options, args = parser.parse_args(args)
4725 auth_config = auth.extract_auth_config_from_options(options)
4726 if args:
4727 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004728
4729 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004730 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004731 # Staged changes would be committed along with the patch from last
4732 # upload, hence counted toward the "last upload" side in the final
4733 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004734 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004735 return 1
4736
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004737 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004738 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004739 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004740 if not issue:
4741 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004742 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004743 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004744
4745 # Create a new branch based on the merge-base
4746 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004747 # Clear cached branch in cl object, to avoid overwriting original CL branch
4748 # properties.
4749 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004750 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004751 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004752 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004753 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004754 return rtn
4755
wychen@chromium.org06928532015-02-03 02:11:29 +00004756 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004757 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00004758 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004759 finally:
4760 RunGit(['checkout', '-q', branch])
4761 RunGit(['branch', '-D', TMP_BRANCH])
4762
4763 return 0
4764
4765
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004766def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004767 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004768 parser.add_option(
4769 '--no-color',
4770 action='store_true',
4771 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004772 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004773 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004774 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004775
4776 author = RunGit(['config', 'user.email']).strip() or None
4777
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004778 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004779
4780 if args:
4781 if len(args) > 1:
4782 parser.error('Unknown args')
4783 base_branch = args[0]
4784 else:
4785 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004786 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004787
4788 change = cl.GetChange(base_branch, None)
4789 return owners_finder.OwnersFinder(
4790 [f.LocalPath() for f in
4791 cl.GetChange(base_branch, None).AffectedFiles()],
4792 change.RepositoryRoot(), author,
4793 fopen=file, os_path=os.path, glob=glob.glob,
4794 disable_color=options.no_color).run()
4795
4796
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004797def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004798 """Generates a diff command."""
4799 # Generate diff for the current branch's changes.
4800 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4801 upstream_commit, '--' ]
4802
4803 if args:
4804 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004805 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004806 diff_cmd.append(arg)
4807 else:
4808 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004809
4810 return diff_cmd
4811
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004812def MatchingFileType(file_name, extensions):
4813 """Returns true if the file name ends with one of the given extensions."""
4814 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004815
enne@chromium.org555cfe42014-01-29 18:21:39 +00004816@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004817def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004818 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00004819 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07004820 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004821 parser.add_option('--full', action='store_true',
4822 help='Reformat the full content of all touched files')
4823 parser.add_option('--dry-run', action='store_true',
4824 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004825 parser.add_option('--python', action='store_true',
4826 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004827 parser.add_option('--diff', action='store_true',
4828 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004829 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004830
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004831 # git diff generates paths against the root of the repository. Change
4832 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004833 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004834 if rel_base_path:
4835 os.chdir(rel_base_path)
4836
digit@chromium.org29e47272013-05-17 17:01:46 +00004837 # Grab the merge-base commit, i.e. the upstream commit of the current
4838 # branch when it was created or the last time it was rebased. This is
4839 # to cover the case where the user may have called "git fetch origin",
4840 # moving the origin branch to a newer commit, but hasn't rebased yet.
4841 upstream_commit = None
4842 cl = Changelist()
4843 upstream_branch = cl.GetUpstreamBranch()
4844 if upstream_branch:
4845 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4846 upstream_commit = upstream_commit.strip()
4847
4848 if not upstream_commit:
4849 DieWithError('Could not find base commit for this branch. '
4850 'Are you in detached state?')
4851
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004852 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4853 diff_output = RunGit(changed_files_cmd)
4854 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004855 # Filter out files deleted by this CL
4856 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004857
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004858 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4859 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4860 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004861 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004862
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004863 top_dir = os.path.normpath(
4864 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
4865
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004866 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4867 # formatted. This is used to block during the presubmit.
4868 return_value = 0
4869
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004870 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004871 # Locate the clang-format binary in the checkout
4872 try:
4873 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07004874 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004875 DieWithError(e)
4876
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004877 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004878 cmd = [clang_format_tool]
4879 if not opts.dry_run and not opts.diff:
4880 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004881 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004882 if opts.diff:
4883 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004884 else:
4885 env = os.environ.copy()
4886 env['PATH'] = str(os.path.dirname(clang_format_tool))
4887 try:
4888 script = clang_format.FindClangFormatScriptInChromiumTree(
4889 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07004890 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004891 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004892
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004893 cmd = [sys.executable, script, '-p0']
4894 if not opts.dry_run and not opts.diff:
4895 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004896
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004897 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
4898 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004899
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004900 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
4901 if opts.diff:
4902 sys.stdout.write(stdout)
4903 if opts.dry_run and len(stdout) > 0:
4904 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004905
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004906 # Similar code to above, but using yapf on .py files rather than clang-format
4907 # on C/C++ files
4908 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004909 yapf_tool = gclient_utils.FindExecutable('yapf')
4910 if yapf_tool is None:
4911 DieWithError('yapf not found in PATH')
4912
4913 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004914 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004915 cmd = [yapf_tool]
4916 if not opts.dry_run and not opts.diff:
4917 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004918 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004919 if opts.diff:
4920 sys.stdout.write(stdout)
4921 else:
4922 # TODO(sbc): yapf --lines mode still has some issues.
4923 # https://github.com/google/yapf/issues/154
4924 DieWithError('--python currently only works with --full')
4925
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004926 # Dart's formatter does not have the nice property of only operating on
4927 # modified chunks, so hard code full.
4928 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004929 try:
4930 command = [dart_format.FindDartFmtToolInChromiumTree()]
4931 if not opts.dry_run and not opts.diff:
4932 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004933 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004934
ppi@chromium.org6593d932016-03-03 15:41:15 +00004935 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004936 if opts.dry_run and stdout:
4937 return_value = 2
4938 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07004939 print('Warning: Unable to check Dart code formatting. Dart SDK not '
4940 'found in this checkout. Files in other languages are still '
4941 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004942
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004943 # Format GN build files. Always run on full build files for canonical form.
4944 if gn_diff_files:
4945 cmd = ['gn', 'format']
4946 if not opts.dry_run and not opts.diff:
4947 cmd.append('--in-place')
4948 for gn_diff_file in gn_diff_files:
bsep@chromium.org627d9002016-04-29 00:00:52 +00004949 stdout = RunCommand(cmd + [gn_diff_file],
4950 shell=sys.platform == 'win32',
4951 cwd=top_dir)
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004952 if opts.diff:
4953 sys.stdout.write(stdout)
4954
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004955 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004956
4957
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004958@subcommand.usage('<codereview url or issue id>')
4959def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004960 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004961 _, args = parser.parse_args(args)
4962
4963 if len(args) != 1:
4964 parser.print_help()
4965 return 1
4966
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004967 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00004968 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004969 parser.print_help()
4970 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00004971 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004972
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004973 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00004974 output = RunGit(['config', '--local', '--get-regexp',
4975 r'branch\..*\.%s' % issueprefix],
4976 error_ok=True)
4977 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004978 if issue == target_issue:
4979 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004980
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004981 branches = []
4982 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00004983 branches.extend(find_issues(cls.IssueSettingSuffix()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004984 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07004985 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004986 return 1
4987 if len(branches) == 1:
4988 RunGit(['checkout', branches[0]])
4989 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004990 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004991 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07004992 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004993 which = raw_input('Choose by index: ')
4994 try:
4995 RunGit(['checkout', branches[int(which)]])
4996 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07004997 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004998 return 1
4999
5000 return 0
5001
5002
maruel@chromium.org29404b52014-09-08 22:58:00 +00005003def CMDlol(parser, args):
5004 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005005 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005006 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5007 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5008 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005009 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005010 return 0
5011
5012
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005013class OptionParser(optparse.OptionParser):
5014 """Creates the option parse and add --verbose support."""
5015 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005016 optparse.OptionParser.__init__(
5017 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005018 self.add_option(
5019 '-v', '--verbose', action='count', default=0,
5020 help='Use 2 times for more debugging info')
5021
5022 def parse_args(self, args=None, values=None):
5023 options, args = optparse.OptionParser.parse_args(self, args, values)
5024 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5025 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5026 return options, args
5027
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005028
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005029def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005030 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005031 print('\nYour python version %s is unsupported, please upgrade.\n' %
5032 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005033 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005034
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005035 # Reload settings.
5036 global settings
5037 settings = Settings()
5038
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005039 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005040 dispatcher = subcommand.CommandDispatcher(__name__)
5041 try:
5042 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005043 except auth.AuthenticationError as e:
5044 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005045 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005046 if e.code != 500:
5047 raise
5048 DieWithError(
5049 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5050 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005051 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005052
5053
5054if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005055 # These affect sys.stdout so do it outside of main() to simplify mocks in
5056 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005057 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005058 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005059 try:
5060 sys.exit(main(sys.argv[1:]))
5061 except KeyboardInterrupt:
5062 sys.stderr.write('interrupted\n')
5063 sys.exit(1)