blob: 52c091f3697543e99c50e22c843936d9ad7d237f [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
maruel@chromium.org0633fb42013-08-16 20:06:14 +000065__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000066
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000067DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000068POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000069DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000070GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
rmistry@google.comc68112d2015-03-03 12:48:06 +000071REFS_THAT_ALIAS_TO_OTHER_REFS = {
72 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
73 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
74}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000075
thestig@chromium.org44202a22014-03-11 19:22:18 +000076# Valid extensions for files we want to lint.
77DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
78DEFAULT_LINT_IGNORE_REGEX = r"$^"
79
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000080# Shortcut since it quickly becomes redundant.
81Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000082
maruel@chromium.orgddd59412011-11-30 14:20:38 +000083# Initialized in main()
84settings = None
85
86
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000087def DieWithError(message):
vapiera7fbd5a2016-06-16 09:17:49 -070088 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000089 sys.exit(1)
90
91
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000092def GetNoGitPagerEnv():
93 env = os.environ.copy()
94 # 'cat' is a magical git string that disables pagers on all platforms.
95 env['GIT_PAGER'] = 'cat'
96 return env
97
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000098
bsep@chromium.org627d9002016-04-29 00:00:52 +000099def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000100 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000101 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000102 except subprocess2.CalledProcessError as e:
103 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000104 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000105 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000106 'Command "%s" failed.\n%s' % (
107 ' '.join(args), error_message or e.stdout or ''))
108 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000109
110
111def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000112 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000113 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000114
115
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000116def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000117 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000118 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000119 if suppress_stderr:
120 stderr = subprocess2.VOID
121 else:
122 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000123 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000124 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000125 stdout=subprocess2.PIPE,
126 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000127 return code, out[0]
128 except ValueError:
129 # When the subprocess fails, it returns None. That triggers a ValueError
130 # when trying to unpack the return value into (out, code).
131 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000132
133
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000134def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000135 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000136 return RunGitWithCode(args, suppress_stderr=True)[1]
137
138
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000139def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000140 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000141 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000142 return (version.startswith(prefix) and
143 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000144
145
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000146def BranchExists(branch):
147 """Return True if specified branch exists."""
148 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
149 suppress_stderr=True)
150 return not code
151
152
maruel@chromium.org90541732011-04-01 17:54:18 +0000153def ask_for_data(prompt):
154 try:
155 return raw_input(prompt)
156 except KeyboardInterrupt:
157 # Hide the exception.
158 sys.exit(1)
159
160
iannucci@chromium.org79540052012-10-19 23:15:26 +0000161def git_set_branch_value(key, value):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000162 branch = GetCurrentBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000163 if not branch:
164 return
165
166 cmd = ['config']
167 if isinstance(value, int):
168 cmd.append('--int')
169 git_key = 'branch.%s.%s' % (branch, key)
170 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000171
172
173def git_get_branch_default(key, default):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000174 branch = GetCurrentBranch()
iannucci@chromium.org79540052012-10-19 23:15:26 +0000175 if branch:
176 git_key = 'branch.%s.%s' % (branch, key)
177 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
178 try:
179 return int(stdout.strip())
180 except ValueError:
181 pass
182 return default
183
184
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000185def add_git_similarity(parser):
186 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000187 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000188 help='Sets the percentage that a pair of files need to match in order to'
189 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000190 parser.add_option(
191 '--find-copies', action='store_true',
192 help='Allows git to look for copies.')
193 parser.add_option(
194 '--no-find-copies', action='store_false', dest='find_copies',
195 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000196
197 old_parser_args = parser.parse_args
198 def Parse(args):
199 options, args = old_parser_args(args)
200
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000201 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000202 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000203 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000204 print('Note: Saving similarity of %d%% in git config.'
205 % options.similarity)
206 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000207
iannucci@chromium.org79540052012-10-19 23:15:26 +0000208 options.similarity = max(0, min(options.similarity, 100))
209
210 if options.find_copies is None:
211 options.find_copies = bool(
212 git_get_branch_default('git-find-copies', True))
213 else:
214 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000215
216 print('Using %d%% similarity for rename/copy detection. '
217 'Override with --similarity.' % options.similarity)
218
219 return options, args
220 parser.parse_args = Parse
221
222
machenbach@chromium.org45453142015-09-15 08:45:22 +0000223def _get_properties_from_options(options):
224 properties = dict(x.split('=', 1) for x in options.properties)
225 for key, val in properties.iteritems():
226 try:
227 properties[key] = json.loads(val)
228 except ValueError:
229 pass # If a value couldn't be evaluated, treat it as a string.
230 return properties
231
232
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000233def _prefix_master(master):
234 """Convert user-specified master name to full master name.
235
236 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
237 name, while the developers always use shortened master name
238 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
239 function does the conversion for buildbucket migration.
240 """
241 prefix = 'master.'
242 if master.startswith(prefix):
243 return master
244 return '%s%s' % (prefix, master)
245
246
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000247def _buildbucket_retry(operation_name, http, *args, **kwargs):
248 """Retries requests to buildbucket service and returns parsed json content."""
249 try_count = 0
250 while True:
251 response, content = http.request(*args, **kwargs)
252 try:
253 content_json = json.loads(content)
254 except ValueError:
255 content_json = None
256
257 # Buildbucket could return an error even if status==200.
258 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000259 error = content_json.get('error')
260 if error.get('code') == 403:
261 raise BuildbucketResponseException(
262 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000263 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000264 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000265 raise BuildbucketResponseException(msg)
266
267 if response.status == 200:
268 if not content_json:
269 raise BuildbucketResponseException(
270 'Buildbucket returns invalid json content: %s.\n'
271 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
272 content)
273 return content_json
274 if response.status < 500 or try_count >= 2:
275 raise httplib2.HttpLib2Error(content)
276
277 # status >= 500 means transient failures.
278 logging.debug('Transient errors when %s. Will retry.', operation_name)
279 time.sleep(0.5 + 1.5*try_count)
280 try_count += 1
281 assert False, 'unreachable'
282
283
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000284def trigger_luci_job(changelist, masters, options):
285 """Send a job to run on LUCI."""
286 issue_props = changelist.GetIssueProperties()
287 issue = changelist.GetIssue()
288 patchset = changelist.GetMostRecentPatchset()
289 for builders_and_tests in sorted(masters.itervalues()):
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000290 # TODO(hinoka et al): add support for other properties.
291 # Currently, this completely ignores testfilter and other properties.
292 for builder in sorted(builders_and_tests):
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000293 luci_trigger.trigger(
294 builder, 'HEAD', issue, patchset, issue_props['project'])
295
296
machenbach@chromium.org45453142015-09-15 08:45:22 +0000297def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000298 rietveld_url = settings.GetDefaultServerUrl()
299 rietveld_host = urlparse.urlparse(rietveld_url).hostname
300 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
301 http = authenticator.authorize(httplib2.Http())
302 http.force_exception_to_status_code = True
303 issue_props = changelist.GetIssueProperties()
304 issue = changelist.GetIssue()
305 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000306 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000307
308 buildbucket_put_url = (
309 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000310 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000311 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
312 hostname=rietveld_host,
313 issue=issue,
314 patch=patchset)
315
316 batch_req_body = {'builds': []}
317 print_text = []
318 print_text.append('Tried jobs on:')
319 for master, builders_and_tests in sorted(masters.iteritems()):
320 print_text.append('Master: %s' % master)
321 bucket = _prefix_master(master)
322 for builder, tests in sorted(builders_and_tests.iteritems()):
323 print_text.append(' %s: %s' % (builder, tests))
324 parameters = {
325 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000326 'changes': [{
327 'author': {'email': issue_props['owner_email']},
328 'revision': options.revision,
329 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000330 'properties': {
331 'category': category,
332 'issue': issue,
333 'master': master,
334 'patch_project': issue_props['project'],
335 'patch_storage': 'rietveld',
336 'patchset': patchset,
337 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000338 'rietveld': rietveld_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000339 },
340 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000341 if 'presubmit' in builder.lower():
342 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000343 if tests:
344 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000345 if properties:
346 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000347 if options.clobber:
348 parameters['properties']['clobber'] = True
349 batch_req_body['builds'].append(
350 {
351 'bucket': bucket,
352 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000353 'client_operation_id': str(uuid.uuid4()),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000354 'tags': ['builder:%s' % builder,
355 'buildset:%s' % buildset,
356 'master:%s' % master,
357 'user_agent:git_cl_try']
358 }
359 )
360
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000361 _buildbucket_retry(
362 'triggering tryjobs',
363 http,
364 buildbucket_put_url,
365 'PUT',
366 body=json.dumps(batch_req_body),
367 headers={'Content-Type': 'application/json'}
368 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000369 print_text.append('To see results here, run: git cl try-results')
370 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700371 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000372
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000373
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000374def fetch_try_jobs(auth_config, changelist, options):
375 """Fetches tryjobs from buildbucket.
376
377 Returns a map from build id to build info as json dictionary.
378 """
379 rietveld_url = settings.GetDefaultServerUrl()
380 rietveld_host = urlparse.urlparse(rietveld_url).hostname
381 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
382 if authenticator.has_cached_credentials():
383 http = authenticator.authorize(httplib2.Http())
384 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700385 print('Warning: Some results might be missing because %s' %
386 # Get the message on how to login.
387 (auth.LoginRequiredError(rietveld_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000388 http = httplib2.Http()
389
390 http.force_exception_to_status_code = True
391
392 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
393 hostname=rietveld_host,
394 issue=changelist.GetIssue(),
395 patch=options.patchset)
396 params = {'tag': 'buildset:%s' % buildset}
397
398 builds = {}
399 while True:
400 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
401 hostname=options.buildbucket_host,
402 params=urllib.urlencode(params))
403 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
404 for build in content.get('builds', []):
405 builds[build['id']] = build
406 if 'next_cursor' in content:
407 params['start_cursor'] = content['next_cursor']
408 else:
409 break
410 return builds
411
412
413def print_tryjobs(options, builds):
414 """Prints nicely result of fetch_try_jobs."""
415 if not builds:
vapiera7fbd5a2016-06-16 09:17:49 -0700416 print('No tryjobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000417 return
418
419 # Make a copy, because we'll be modifying builds dictionary.
420 builds = builds.copy()
421 builder_names_cache = {}
422
423 def get_builder(b):
424 try:
425 return builder_names_cache[b['id']]
426 except KeyError:
427 try:
428 parameters = json.loads(b['parameters_json'])
429 name = parameters['builder_name']
430 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700431 print('WARNING: failed to get builder name for build %s: %s' % (
432 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000433 name = None
434 builder_names_cache[b['id']] = name
435 return name
436
437 def get_bucket(b):
438 bucket = b['bucket']
439 if bucket.startswith('master.'):
440 return bucket[len('master.'):]
441 return bucket
442
443 if options.print_master:
444 name_fmt = '%%-%ds %%-%ds' % (
445 max(len(str(get_bucket(b))) for b in builds.itervalues()),
446 max(len(str(get_builder(b))) for b in builds.itervalues()))
447 def get_name(b):
448 return name_fmt % (get_bucket(b), get_builder(b))
449 else:
450 name_fmt = '%%-%ds' % (
451 max(len(str(get_builder(b))) for b in builds.itervalues()))
452 def get_name(b):
453 return name_fmt % get_builder(b)
454
455 def sort_key(b):
456 return b['status'], b.get('result'), get_name(b), b.get('url')
457
458 def pop(title, f, color=None, **kwargs):
459 """Pop matching builds from `builds` dict and print them."""
460
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000461 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000462 colorize = str
463 else:
464 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
465
466 result = []
467 for b in builds.values():
468 if all(b.get(k) == v for k, v in kwargs.iteritems()):
469 builds.pop(b['id'])
470 result.append(b)
471 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700472 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000473 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700474 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000475
476 total = len(builds)
477 pop(status='COMPLETED', result='SUCCESS',
478 title='Successes:', color=Fore.GREEN,
479 f=lambda b: (get_name(b), b.get('url')))
480 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
481 title='Infra Failures:', color=Fore.MAGENTA,
482 f=lambda b: (get_name(b), b.get('url')))
483 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
484 title='Failures:', color=Fore.RED,
485 f=lambda b: (get_name(b), b.get('url')))
486 pop(status='COMPLETED', result='CANCELED',
487 title='Canceled:', color=Fore.MAGENTA,
488 f=lambda b: (get_name(b),))
489 pop(status='COMPLETED', result='FAILURE',
490 failure_reason='INVALID_BUILD_DEFINITION',
491 title='Wrong master/builder name:', color=Fore.MAGENTA,
492 f=lambda b: (get_name(b),))
493 pop(status='COMPLETED', result='FAILURE',
494 title='Other failures:',
495 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
496 pop(status='COMPLETED',
497 title='Other finished:',
498 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
499 pop(status='STARTED',
500 title='Started:', color=Fore.YELLOW,
501 f=lambda b: (get_name(b), b.get('url')))
502 pop(status='SCHEDULED',
503 title='Scheduled:',
504 f=lambda b: (get_name(b), 'id=%s' % b['id']))
505 # The last section is just in case buildbucket API changes OR there is a bug.
506 pop(title='Other:',
507 f=lambda b: (get_name(b), 'id=%s' % b['id']))
508 assert len(builds) == 0
vapiera7fbd5a2016-06-16 09:17:49 -0700509 print('Total: %d tryjobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000510
511
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000512def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
513 """Return the corresponding git ref if |base_url| together with |glob_spec|
514 matches the full |url|.
515
516 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
517 """
518 fetch_suburl, as_ref = glob_spec.split(':')
519 if allow_wildcards:
520 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
521 if glob_match:
522 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
523 # "branches/{472,597,648}/src:refs/remotes/svn/*".
524 branch_re = re.escape(base_url)
525 if glob_match.group(1):
526 branch_re += '/' + re.escape(glob_match.group(1))
527 wildcard = glob_match.group(2)
528 if wildcard == '*':
529 branch_re += '([^/]*)'
530 else:
531 # Escape and replace surrounding braces with parentheses and commas
532 # with pipe symbols.
533 wildcard = re.escape(wildcard)
534 wildcard = re.sub('^\\\\{', '(', wildcard)
535 wildcard = re.sub('\\\\,', '|', wildcard)
536 wildcard = re.sub('\\\\}$', ')', wildcard)
537 branch_re += wildcard
538 if glob_match.group(3):
539 branch_re += re.escape(glob_match.group(3))
540 match = re.match(branch_re, url)
541 if match:
542 return re.sub('\*$', match.group(1), as_ref)
543
544 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
545 if fetch_suburl:
546 full_url = base_url + '/' + fetch_suburl
547 else:
548 full_url = base_url
549 if full_url == url:
550 return as_ref
551 return None
552
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000553
iannucci@chromium.org79540052012-10-19 23:15:26 +0000554def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000555 """Prints statistics about the change to the user."""
556 # --no-ext-diff is broken in some versions of Git, so try to work around
557 # this by overriding the environment (but there is still a problem if the
558 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000559 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000560 if 'GIT_EXTERNAL_DIFF' in env:
561 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000562
563 if find_copies:
564 similarity_options = ['--find-copies-harder', '-l100000',
565 '-C%s' % similarity]
566 else:
567 similarity_options = ['-M%s' % similarity]
568
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000569 try:
570 stdout = sys.stdout.fileno()
571 except AttributeError:
572 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000573 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000574 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000575 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000576 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000577
578
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000579class BuildbucketResponseException(Exception):
580 pass
581
582
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000583class Settings(object):
584 def __init__(self):
585 self.default_server = None
586 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000587 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000588 self.is_git_svn = None
589 self.svn_branch = None
590 self.tree_status_url = None
591 self.viewvc_url = None
592 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000593 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000594 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000595 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000596 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000597 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000598 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000599 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000600
601 def LazyUpdateIfNeeded(self):
602 """Updates the settings from a codereview.settings file, if available."""
603 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000604 # The only value that actually changes the behavior is
605 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000606 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000607 error_ok=True
608 ).strip().lower()
609
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000610 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000611 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000612 LoadCodereviewSettingsFromFile(cr_settings_file)
613 self.updated = True
614
615 def GetDefaultServerUrl(self, error_ok=False):
616 if not self.default_server:
617 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000618 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000619 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000620 if error_ok:
621 return self.default_server
622 if not self.default_server:
623 error_message = ('Could not find settings file. You must configure '
624 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000625 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000626 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000627 return self.default_server
628
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000629 @staticmethod
630 def GetRelativeRoot():
631 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000632
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000633 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000634 if self.root is None:
635 self.root = os.path.abspath(self.GetRelativeRoot())
636 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000637
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000638 def GetGitMirror(self, remote='origin'):
639 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000640 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000641 if not os.path.isdir(local_url):
642 return None
643 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
644 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
645 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
646 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
647 if mirror.exists():
648 return mirror
649 return None
650
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000651 def GetIsGitSvn(self):
652 """Return true if this repo looks like it's using git-svn."""
653 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000654 if self.GetPendingRefPrefix():
655 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
656 self.is_git_svn = False
657 else:
658 # If you have any "svn-remote.*" config keys, we think you're using svn.
659 self.is_git_svn = RunGitWithCode(
660 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000661 return self.is_git_svn
662
663 def GetSVNBranch(self):
664 if self.svn_branch is None:
665 if not self.GetIsGitSvn():
666 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
667
668 # Try to figure out which remote branch we're based on.
669 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000670 # 1) iterate through our branch history and find the svn URL.
671 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000672
673 # regexp matching the git-svn line that contains the URL.
674 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
675
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000676 # We don't want to go through all of history, so read a line from the
677 # pipe at a time.
678 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000679 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000680 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
681 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000682 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000683 for line in proc.stdout:
684 match = git_svn_re.match(line)
685 if match:
686 url = match.group(1)
687 proc.stdout.close() # Cut pipe.
688 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000689
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000690 if url:
691 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
692 remotes = RunGit(['config', '--get-regexp',
693 r'^svn-remote\..*\.url']).splitlines()
694 for remote in remotes:
695 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000696 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000697 remote = match.group(1)
698 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000699 rewrite_root = RunGit(
700 ['config', 'svn-remote.%s.rewriteRoot' % remote],
701 error_ok=True).strip()
702 if rewrite_root:
703 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000704 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000705 ['config', 'svn-remote.%s.fetch' % remote],
706 error_ok=True).strip()
707 if fetch_spec:
708 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
709 if self.svn_branch:
710 break
711 branch_spec = RunGit(
712 ['config', 'svn-remote.%s.branches' % remote],
713 error_ok=True).strip()
714 if branch_spec:
715 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
716 if self.svn_branch:
717 break
718 tag_spec = RunGit(
719 ['config', 'svn-remote.%s.tags' % remote],
720 error_ok=True).strip()
721 if tag_spec:
722 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
723 if self.svn_branch:
724 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000725
726 if not self.svn_branch:
727 DieWithError('Can\'t guess svn branch -- try specifying it on the '
728 'command line')
729
730 return self.svn_branch
731
732 def GetTreeStatusUrl(self, error_ok=False):
733 if not self.tree_status_url:
734 error_message = ('You must configure your tree status URL by running '
735 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000736 self.tree_status_url = self._GetRietveldConfig(
737 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000738 return self.tree_status_url
739
740 def GetViewVCUrl(self):
741 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000742 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000743 return self.viewvc_url
744
rmistry@google.com90752582014-01-14 21:04:50 +0000745 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000746 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000747
rmistry@google.com78948ed2015-07-08 23:09:57 +0000748 def GetIsSkipDependencyUpload(self, branch_name):
749 """Returns true if specified branch should skip dep uploads."""
750 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
751 error_ok=True)
752
rmistry@google.com5626a922015-02-26 14:03:30 +0000753 def GetRunPostUploadHook(self):
754 run_post_upload_hook = self._GetRietveldConfig(
755 'run-post-upload-hook', error_ok=True)
756 return run_post_upload_hook == "True"
757
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000758 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000759 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000760
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000761 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000762 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000763
ukai@chromium.orge8077812012-02-03 03:41:46 +0000764 def GetIsGerrit(self):
765 """Return true if this repo is assosiated with gerrit code review system."""
766 if self.is_gerrit is None:
767 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
768 return self.is_gerrit
769
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000770 def GetSquashGerritUploads(self):
771 """Return true if uploads to Gerrit should be squashed by default."""
772 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700773 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
774 if self.squash_gerrit_uploads is None:
775 # Default is squash now (http://crbug.com/611892#c23).
776 self.squash_gerrit_uploads = not (
777 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
778 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000779 return self.squash_gerrit_uploads
780
tandriia60502f2016-06-20 02:01:53 -0700781 def GetSquashGerritUploadsOverride(self):
782 """Return True or False if codereview.settings should be overridden.
783
784 Returns None if no override has been defined.
785 """
786 # See also http://crbug.com/611892#c23
787 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
788 error_ok=True).strip()
789 if result == 'true':
790 return True
791 if result == 'false':
792 return False
793 return None
794
tandrii@chromium.org28253532016-04-14 13:46:56 +0000795 def GetGerritSkipEnsureAuthenticated(self):
796 """Return True if EnsureAuthenticated should not be done for Gerrit
797 uploads."""
798 if self.gerrit_skip_ensure_authenticated is None:
799 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000800 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000801 error_ok=True).strip() == 'true')
802 return self.gerrit_skip_ensure_authenticated
803
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000804 def GetGitEditor(self):
805 """Return the editor specified in the git config, or None if none is."""
806 if self.git_editor is None:
807 self.git_editor = self._GetConfig('core.editor', error_ok=True)
808 return self.git_editor or None
809
thestig@chromium.org44202a22014-03-11 19:22:18 +0000810 def GetLintRegex(self):
811 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
812 DEFAULT_LINT_REGEX)
813
814 def GetLintIgnoreRegex(self):
815 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
816 DEFAULT_LINT_IGNORE_REGEX)
817
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000818 def GetProject(self):
819 if not self.project:
820 self.project = self._GetRietveldConfig('project', error_ok=True)
821 return self.project
822
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000823 def GetForceHttpsCommitUrl(self):
824 if not self.force_https_commit_url:
825 self.force_https_commit_url = self._GetRietveldConfig(
826 'force-https-commit-url', error_ok=True)
827 return self.force_https_commit_url
828
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000829 def GetPendingRefPrefix(self):
830 if not self.pending_ref_prefix:
831 self.pending_ref_prefix = self._GetRietveldConfig(
832 'pending-ref-prefix', error_ok=True)
833 return self.pending_ref_prefix
834
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000835 def _GetRietveldConfig(self, param, **kwargs):
836 return self._GetConfig('rietveld.' + param, **kwargs)
837
rmistry@google.com78948ed2015-07-08 23:09:57 +0000838 def _GetBranchConfig(self, branch_name, param, **kwargs):
839 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
840
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000841 def _GetConfig(self, param, **kwargs):
842 self.LazyUpdateIfNeeded()
843 return RunGit(['config', param], **kwargs).strip()
844
845
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000846def ShortBranchName(branch):
847 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000848 return branch.replace('refs/heads/', '', 1)
849
850
851def GetCurrentBranchRef():
852 """Returns branch ref (e.g., refs/heads/master) or None."""
853 return RunGit(['symbolic-ref', 'HEAD'],
854 stderr=subprocess2.VOID, error_ok=True).strip() or None
855
856
857def GetCurrentBranch():
858 """Returns current branch or None.
859
860 For refs/heads/* branches, returns just last part. For others, full ref.
861 """
862 branchref = GetCurrentBranchRef()
863 if branchref:
864 return ShortBranchName(branchref)
865 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000866
867
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000868class _CQState(object):
869 """Enum for states of CL with respect to Commit Queue."""
870 NONE = 'none'
871 DRY_RUN = 'dry_run'
872 COMMIT = 'commit'
873
874 ALL_STATES = [NONE, DRY_RUN, COMMIT]
875
876
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000877class _ParsedIssueNumberArgument(object):
878 def __init__(self, issue=None, patchset=None, hostname=None):
879 self.issue = issue
880 self.patchset = patchset
881 self.hostname = hostname
882
883 @property
884 def valid(self):
885 return self.issue is not None
886
887
888class _RietveldParsedIssueNumberArgument(_ParsedIssueNumberArgument):
889 def __init__(self, *args, **kwargs):
890 self.patch_url = kwargs.pop('patch_url', None)
891 super(_RietveldParsedIssueNumberArgument, self).__init__(*args, **kwargs)
892
893
894def ParseIssueNumberArgument(arg):
895 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
896 fail_result = _ParsedIssueNumberArgument()
897
898 if arg.isdigit():
899 return _ParsedIssueNumberArgument(issue=int(arg))
900 if not arg.startswith('http'):
901 return fail_result
902 url = gclient_utils.UpgradeToHttps(arg)
903 try:
904 parsed_url = urlparse.urlparse(url)
905 except ValueError:
906 return fail_result
907 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
908 tmp = cls.ParseIssueURL(parsed_url)
909 if tmp is not None:
910 return tmp
911 return fail_result
912
913
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000914class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000915 """Changelist works with one changelist in local branch.
916
917 Supports two codereview backends: Rietveld or Gerrit, selected at object
918 creation.
919
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +0000920 Notes:
921 * Not safe for concurrent multi-{thread,process} use.
922 * Caches values from current branch. Therefore, re-use after branch change
923 with care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000924 """
925
926 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
927 """Create a new ChangeList instance.
928
929 If issue is given, the codereview must be given too.
930
931 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
932 Otherwise, it's decided based on current configuration of the local branch,
933 with default being 'rietveld' for backwards compatibility.
934 See _load_codereview_impl for more details.
935
936 **kwargs will be passed directly to codereview implementation.
937 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000938 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000939 global settings
940 if not settings:
941 # Happens when git_cl.py is used as a utility library.
942 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000943
944 if issue:
945 assert codereview, 'codereview must be known, if issue is known'
946
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000947 self.branchref = branchref
948 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000949 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000950 self.branch = ShortBranchName(self.branchref)
951 else:
952 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000953 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000954 self.lookedup_issue = False
955 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000956 self.has_description = False
957 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000958 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000959 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000960 self.cc = None
961 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000962 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000963
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000964 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000965 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000966 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000967 assert self._codereview_impl
968 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000969
970 def _load_codereview_impl(self, codereview=None, **kwargs):
971 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000972 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
973 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
974 self._codereview = codereview
975 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000976 return
977
978 # Automatic selection based on issue number set for a current branch.
979 # Rietveld takes precedence over Gerrit.
980 assert not self.issue
981 # Whether we find issue or not, we are doing the lookup.
982 self.lookedup_issue = True
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000983 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000984 setting = cls.IssueSetting(self.GetBranch())
985 issue = RunGit(['config', setting], error_ok=True).strip()
986 if issue:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000987 self._codereview = codereview
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000988 self._codereview_impl = cls(self, **kwargs)
989 self.issue = int(issue)
990 return
991
992 # No issue is set for this branch, so decide based on repo-wide settings.
993 return self._load_codereview_impl(
994 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
995 **kwargs)
996
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000997 def IsGerrit(self):
998 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000999
1000 def GetCCList(self):
1001 """Return the users cc'd on this CL.
1002
1003 Return is a string suitable for passing to gcl with the --cc flag.
1004 """
1005 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001006 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001007 more_cc = ','.join(self.watchers)
1008 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1009 return self.cc
1010
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001011 def GetCCListWithoutDefault(self):
1012 """Return the users cc'd on this CL excluding default ones."""
1013 if self.cc is None:
1014 self.cc = ','.join(self.watchers)
1015 return self.cc
1016
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001017 def SetWatchers(self, watchers):
1018 """Set the list of email addresses that should be cc'd based on the changed
1019 files in this CL.
1020 """
1021 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001022
1023 def GetBranch(self):
1024 """Returns the short branch name, e.g. 'master'."""
1025 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001026 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001027 if not branchref:
1028 return None
1029 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001030 self.branch = ShortBranchName(self.branchref)
1031 return self.branch
1032
1033 def GetBranchRef(self):
1034 """Returns the full branch name, e.g. 'refs/heads/master'."""
1035 self.GetBranch() # Poke the lazy loader.
1036 return self.branchref
1037
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001038 def ClearBranch(self):
1039 """Clears cached branch data of this object."""
1040 self.branch = self.branchref = None
1041
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001042 @staticmethod
1043 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001044 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001045 e.g. 'origin', 'refs/heads/master'
1046 """
1047 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001048 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
1049 error_ok=True).strip()
1050 if upstream_branch:
1051 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
1052 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001053 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1054 error_ok=True).strip()
1055 if upstream_branch:
1056 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001057 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001058 # Fall back on trying a git-svn upstream branch.
1059 if settings.GetIsGitSvn():
1060 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001061 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001062 # Else, try to guess the origin remote.
1063 remote_branches = RunGit(['branch', '-r']).split()
1064 if 'origin/master' in remote_branches:
1065 # Fall back on origin/master if it exits.
1066 remote = 'origin'
1067 upstream_branch = 'refs/heads/master'
1068 elif 'origin/trunk' in remote_branches:
1069 # Fall back on origin/trunk if it exists. Generally a shared
1070 # git-svn clone
1071 remote = 'origin'
1072 upstream_branch = 'refs/heads/trunk'
1073 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001074 DieWithError(
1075 'Unable to determine default branch to diff against.\n'
1076 'Either pass complete "git diff"-style arguments, like\n'
1077 ' git cl upload origin/master\n'
1078 'or verify this branch is set up to track another \n'
1079 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001080
1081 return remote, upstream_branch
1082
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001083 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001084 upstream_branch = self.GetUpstreamBranch()
1085 if not BranchExists(upstream_branch):
1086 DieWithError('The upstream for the current branch (%s) does not exist '
1087 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001088 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001089 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001090
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001091 def GetUpstreamBranch(self):
1092 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001093 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001094 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001095 upstream_branch = upstream_branch.replace('refs/heads/',
1096 'refs/remotes/%s/' % remote)
1097 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1098 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001099 self.upstream_branch = upstream_branch
1100 return self.upstream_branch
1101
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001102 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001103 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001104 remote, branch = None, self.GetBranch()
1105 seen_branches = set()
1106 while branch not in seen_branches:
1107 seen_branches.add(branch)
1108 remote, branch = self.FetchUpstreamTuple(branch)
1109 branch = ShortBranchName(branch)
1110 if remote != '.' or branch.startswith('refs/remotes'):
1111 break
1112 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001113 remotes = RunGit(['remote'], error_ok=True).split()
1114 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001115 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001116 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001117 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001118 logging.warning('Could not determine which remote this change is '
1119 'associated with, so defaulting to "%s". This may '
1120 'not be what you want. You may prevent this message '
1121 'by running "git svn info" as documented here: %s',
1122 self._remote,
1123 GIT_INSTRUCTIONS_URL)
1124 else:
1125 logging.warn('Could not determine which remote this change is '
1126 'associated with. You may prevent this message by '
1127 'running "git svn info" as documented here: %s',
1128 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001129 branch = 'HEAD'
1130 if branch.startswith('refs/remotes'):
1131 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001132 elif branch.startswith('refs/branch-heads/'):
1133 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001134 else:
1135 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001136 return self._remote
1137
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001138 def GitSanityChecks(self, upstream_git_obj):
1139 """Checks git repo status and ensures diff is from local commits."""
1140
sbc@chromium.org79706062015-01-14 21:18:12 +00001141 if upstream_git_obj is None:
1142 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001143 print('ERROR: unable to determine current branch (detached HEAD?)',
1144 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001145 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001146 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001147 return False
1148
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001149 # Verify the commit we're diffing against is in our current branch.
1150 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1151 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1152 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001153 print('ERROR: %s is not in the current branch. You may need to rebase '
1154 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001155 return False
1156
1157 # List the commits inside the diff, and verify they are all local.
1158 commits_in_diff = RunGit(
1159 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1160 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1161 remote_branch = remote_branch.strip()
1162 if code != 0:
1163 _, remote_branch = self.GetRemoteBranch()
1164
1165 commits_in_remote = RunGit(
1166 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1167
1168 common_commits = set(commits_in_diff) & set(commits_in_remote)
1169 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001170 print('ERROR: Your diff contains %d commits already in %s.\n'
1171 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1172 'the diff. If you are using a custom git flow, you can override'
1173 ' the reference used for this check with "git config '
1174 'gitcl.remotebranch <git-ref>".' % (
1175 len(common_commits), remote_branch, upstream_git_obj),
1176 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001177 return False
1178 return True
1179
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001180 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001181 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001182
1183 Returns None if it is not set.
1184 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001185 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
1186 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001187
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001188 def GetGitSvnRemoteUrl(self):
1189 """Return the configured git-svn remote URL parsed from git svn info.
1190
1191 Returns None if it is not set.
1192 """
1193 # URL is dependent on the current directory.
1194 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1195 if data:
1196 keys = dict(line.split(': ', 1) for line in data.splitlines()
1197 if ': ' in line)
1198 return keys.get('URL', None)
1199 return None
1200
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001201 def GetRemoteUrl(self):
1202 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1203
1204 Returns None if there is no remote.
1205 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001206 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001207 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1208
1209 # If URL is pointing to a local directory, it is probably a git cache.
1210 if os.path.isdir(url):
1211 url = RunGit(['config', 'remote.%s.url' % remote],
1212 error_ok=True,
1213 cwd=url).strip()
1214 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001215
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001216 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001217 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001218 if self.issue is None and not self.lookedup_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001219 issue = RunGit(['config',
1220 self._codereview_impl.IssueSetting(self.GetBranch())],
1221 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001222 self.issue = int(issue) or None if issue else None
1223 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001224 return self.issue
1225
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001226 def GetIssueURL(self):
1227 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001228 issue = self.GetIssue()
1229 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001230 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001231 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001232
1233 def GetDescription(self, pretty=False):
1234 if not self.has_description:
1235 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001236 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001237 self.has_description = True
1238 if pretty:
1239 wrapper = textwrap.TextWrapper()
1240 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1241 return wrapper.fill(self.description)
1242 return self.description
1243
1244 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001245 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001246 if self.patchset is None and not self.lookedup_patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001247 patchset = RunGit(['config', self._codereview_impl.PatchsetSetting()],
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001248 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001249 self.patchset = int(patchset) or None if patchset else None
1250 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001251 return self.patchset
1252
1253 def SetPatchset(self, patchset):
1254 """Set this branch's patchset. If patchset=0, clears the patchset."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001255 patchset_setting = self._codereview_impl.PatchsetSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001256 if patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001257 RunGit(['config', patchset_setting, str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001258 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001259 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001260 RunGit(['config', '--unset', patchset_setting],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001261 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001262 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001263
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001264 def SetIssue(self, issue=None):
1265 """Set this branch's issue. If issue isn't given, clears the issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001266 issue_setting = self._codereview_impl.IssueSetting(self.GetBranch())
1267 codereview_setting = self._codereview_impl.GetCodereviewServerSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001268 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001269 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001270 RunGit(['config', issue_setting, str(issue)])
1271 codereview_server = self._codereview_impl.GetCodereviewServer()
1272 if codereview_server:
1273 RunGit(['config', codereview_setting, codereview_server])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001274 else:
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001275 # Reset it regardless. It doesn't hurt.
1276 config_settings = [issue_setting, self._codereview_impl.PatchsetSetting()]
1277 for prop in (['last-upload-hash'] +
1278 self._codereview_impl._PostUnsetIssueProperties()):
1279 config_settings.append('branch.%s.%s' % (self.GetBranch(), prop))
1280 for setting in config_settings:
1281 RunGit(['config', '--unset', setting], error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001282 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001283 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001284
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001285 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001286 if not self.GitSanityChecks(upstream_branch):
1287 DieWithError('\nGit sanity check failure')
1288
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001289 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001290 if not root:
1291 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001292 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001293
1294 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001295 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001296 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001297 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001298 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001299 except subprocess2.CalledProcessError:
1300 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001301 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001302 'This branch probably doesn\'t exist anymore. To reset the\n'
1303 'tracking branch, please run\n'
1304 ' git branch --set-upstream %s trunk\n'
1305 'replacing trunk with origin/master or the relevant branch') %
1306 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001307
maruel@chromium.org52424302012-08-29 15:14:30 +00001308 issue = self.GetIssue()
1309 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001310 if issue:
1311 description = self.GetDescription()
1312 else:
1313 # If the change was never uploaded, use the log messages of all commits
1314 # up to the branch point, as git cl upload will prefill the description
1315 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001316 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1317 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001318
1319 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001320 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001321 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001322 name,
1323 description,
1324 absroot,
1325 files,
1326 issue,
1327 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001328 author,
1329 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001330
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001331 def UpdateDescription(self, description):
1332 self.description = description
1333 return self._codereview_impl.UpdateDescriptionRemote(description)
1334
1335 def RunHook(self, committing, may_prompt, verbose, change):
1336 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1337 try:
1338 return presubmit_support.DoPresubmitChecks(change, committing,
1339 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1340 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001341 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1342 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001343 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001344 DieWithError(
1345 ('%s\nMaybe your depot_tools is out of date?\n'
1346 'If all fails, contact maruel@') % e)
1347
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001348 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1349 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001350 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1351 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001352 else:
1353 # Assume url.
1354 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1355 urlparse.urlparse(issue_arg))
1356 if not parsed_issue_arg or not parsed_issue_arg.valid:
1357 DieWithError('Failed to parse issue argument "%s". '
1358 'Must be an issue number or a valid URL.' % issue_arg)
1359 return self._codereview_impl.CMDPatchWithParsedIssue(
1360 parsed_issue_arg, reject, nocommit, directory)
1361
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001362 def CMDUpload(self, options, git_diff_args, orig_args):
1363 """Uploads a change to codereview."""
1364 if git_diff_args:
1365 # TODO(ukai): is it ok for gerrit case?
1366 base_branch = git_diff_args[0]
1367 else:
1368 if self.GetBranch() is None:
1369 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1370
1371 # Default to diffing against common ancestor of upstream branch
1372 base_branch = self.GetCommonAncestorWithUpstream()
1373 git_diff_args = [base_branch, 'HEAD']
1374
1375 # Make sure authenticated to codereview before running potentially expensive
1376 # hooks. It is a fast, best efforts check. Codereview still can reject the
1377 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001378 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001379
1380 # Apply watchlists on upload.
1381 change = self.GetChange(base_branch, None)
1382 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1383 files = [f.LocalPath() for f in change.AffectedFiles()]
1384 if not options.bypass_watchlists:
1385 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1386
1387 if not options.bypass_hooks:
1388 if options.reviewers or options.tbr_owners:
1389 # Set the reviewer list now so that presubmit checks can access it.
1390 change_description = ChangeDescription(change.FullDescriptionText())
1391 change_description.update_reviewers(options.reviewers,
1392 options.tbr_owners,
1393 change)
1394 change.SetDescriptionText(change_description.description)
1395 hook_results = self.RunHook(committing=False,
1396 may_prompt=not options.force,
1397 verbose=options.verbose,
1398 change=change)
1399 if not hook_results.should_continue():
1400 return 1
1401 if not options.reviewers and hook_results.reviewers:
1402 options.reviewers = hook_results.reviewers.split(',')
1403
1404 if self.GetIssue():
1405 latest_patchset = self.GetMostRecentPatchset()
1406 local_patchset = self.GetPatchset()
1407 if (latest_patchset and local_patchset and
1408 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001409 print('The last upload made from this repository was patchset #%d but '
1410 'the most recent patchset on the server is #%d.'
1411 % (local_patchset, latest_patchset))
1412 print('Uploading will still work, but if you\'ve uploaded to this '
1413 'issue from another machine or branch the patch you\'re '
1414 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001415 ask_for_data('About to upload; enter to confirm.')
1416
1417 print_stats(options.similarity, options.find_copies, git_diff_args)
1418 ret = self.CMDUploadChange(options, git_diff_args, change)
1419 if not ret:
1420 git_set_branch_value('last-upload-hash',
1421 RunGit(['rev-parse', 'HEAD']).strip())
1422 # Run post upload hooks, if specified.
1423 if settings.GetRunPostUploadHook():
1424 presubmit_support.DoPostUploadExecuter(
1425 change,
1426 self,
1427 settings.GetRoot(),
1428 options.verbose,
1429 sys.stdout)
1430
1431 # Upload all dependencies if specified.
1432 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001433 print()
1434 print('--dependencies has been specified.')
1435 print('All dependent local branches will be re-uploaded.')
1436 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001437 # Remove the dependencies flag from args so that we do not end up in a
1438 # loop.
1439 orig_args.remove('--dependencies')
1440 ret = upload_branch_deps(self, orig_args)
1441 return ret
1442
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001443 def SetCQState(self, new_state):
1444 """Update the CQ state for latest patchset.
1445
1446 Issue must have been already uploaded and known.
1447 """
1448 assert new_state in _CQState.ALL_STATES
1449 assert self.GetIssue()
1450 return self._codereview_impl.SetCQState(new_state)
1451
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001452 # Forward methods to codereview specific implementation.
1453
1454 def CloseIssue(self):
1455 return self._codereview_impl.CloseIssue()
1456
1457 def GetStatus(self):
1458 return self._codereview_impl.GetStatus()
1459
1460 def GetCodereviewServer(self):
1461 return self._codereview_impl.GetCodereviewServer()
1462
1463 def GetApprovingReviewers(self):
1464 return self._codereview_impl.GetApprovingReviewers()
1465
1466 def GetMostRecentPatchset(self):
1467 return self._codereview_impl.GetMostRecentPatchset()
1468
1469 def __getattr__(self, attr):
1470 # This is because lots of untested code accesses Rietveld-specific stuff
1471 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001472 # on a case by case basis.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001473 return getattr(self._codereview_impl, attr)
1474
1475
1476class _ChangelistCodereviewBase(object):
1477 """Abstract base class encapsulating codereview specifics of a changelist."""
1478 def __init__(self, changelist):
1479 self._changelist = changelist # instance of Changelist
1480
1481 def __getattr__(self, attr):
1482 # Forward methods to changelist.
1483 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1484 # _RietveldChangelistImpl to avoid this hack?
1485 return getattr(self._changelist, attr)
1486
1487 def GetStatus(self):
1488 """Apply a rough heuristic to give a simple summary of an issue's review
1489 or CQ status, assuming adherence to a common workflow.
1490
1491 Returns None if no issue for this branch, or specific string keywords.
1492 """
1493 raise NotImplementedError()
1494
1495 def GetCodereviewServer(self):
1496 """Returns server URL without end slash, like "https://codereview.com"."""
1497 raise NotImplementedError()
1498
1499 def FetchDescription(self):
1500 """Fetches and returns description from the codereview server."""
1501 raise NotImplementedError()
1502
1503 def GetCodereviewServerSetting(self):
1504 """Returns git config setting for the codereview server."""
1505 raise NotImplementedError()
1506
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001507 @classmethod
1508 def IssueSetting(cls, branch):
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001509 return 'branch.%s.%s' % (branch, cls.IssueSettingSuffix())
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001510
1511 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001512 def IssueSettingSuffix(cls):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001513 """Returns name of git config setting which stores issue number for a given
1514 branch."""
1515 raise NotImplementedError()
1516
1517 def PatchsetSetting(self):
1518 """Returns name of git config setting which stores issue number."""
1519 raise NotImplementedError()
1520
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001521 def _PostUnsetIssueProperties(self):
1522 """Which branch-specific properties to erase when unsettin issue."""
1523 raise NotImplementedError()
1524
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001525 def GetRieveldObjForPresubmit(self):
1526 # This is an unfortunate Rietveld-embeddedness in presubmit.
1527 # For non-Rietveld codereviews, this probably should return a dummy object.
1528 raise NotImplementedError()
1529
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001530 def GetGerritObjForPresubmit(self):
1531 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1532 return None
1533
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001534 def UpdateDescriptionRemote(self, description):
1535 """Update the description on codereview site."""
1536 raise NotImplementedError()
1537
1538 def CloseIssue(self):
1539 """Closes the issue."""
1540 raise NotImplementedError()
1541
1542 def GetApprovingReviewers(self):
1543 """Returns a list of reviewers approving the change.
1544
1545 Note: not necessarily committers.
1546 """
1547 raise NotImplementedError()
1548
1549 def GetMostRecentPatchset(self):
1550 """Returns the most recent patchset number from the codereview site."""
1551 raise NotImplementedError()
1552
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001553 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1554 directory):
1555 """Fetches and applies the issue.
1556
1557 Arguments:
1558 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1559 reject: if True, reject the failed patch instead of switching to 3-way
1560 merge. Rietveld only.
1561 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1562 only.
1563 directory: switch to directory before applying the patch. Rietveld only.
1564 """
1565 raise NotImplementedError()
1566
1567 @staticmethod
1568 def ParseIssueURL(parsed_url):
1569 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1570 failed."""
1571 raise NotImplementedError()
1572
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001573 def EnsureAuthenticated(self, force):
1574 """Best effort check that user is authenticated with codereview server.
1575
1576 Arguments:
1577 force: whether to skip confirmation questions.
1578 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001579 raise NotImplementedError()
1580
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001581 def CMDUploadChange(self, options, args, change):
1582 """Uploads a change to codereview."""
1583 raise NotImplementedError()
1584
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001585 def SetCQState(self, new_state):
1586 """Update the CQ state for latest patchset.
1587
1588 Issue must have been already uploaded and known.
1589 """
1590 raise NotImplementedError()
1591
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001592
1593class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1594 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1595 super(_RietveldChangelistImpl, self).__init__(changelist)
1596 assert settings, 'must be initialized in _ChangelistCodereviewBase'
1597 settings.GetDefaultServerUrl()
1598
1599 self._rietveld_server = rietveld_server
1600 self._auth_config = auth_config
1601 self._props = None
1602 self._rpc_server = None
1603
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001604 def GetCodereviewServer(self):
1605 if not self._rietveld_server:
1606 # If we're on a branch then get the server potentially associated
1607 # with that branch.
1608 if self.GetIssue():
1609 rietveld_server_setting = self.GetCodereviewServerSetting()
1610 if rietveld_server_setting:
1611 self._rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1612 ['config', rietveld_server_setting], error_ok=True).strip())
1613 if not self._rietveld_server:
1614 self._rietveld_server = settings.GetDefaultServerUrl()
1615 return self._rietveld_server
1616
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001617 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001618 """Best effort check that user is authenticated with Rietveld server."""
1619 if self._auth_config.use_oauth2:
1620 authenticator = auth.get_authenticator_for_host(
1621 self.GetCodereviewServer(), self._auth_config)
1622 if not authenticator.has_cached_credentials():
1623 raise auth.LoginRequiredError(self.GetCodereviewServer())
1624
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001625 def FetchDescription(self):
1626 issue = self.GetIssue()
1627 assert issue
1628 try:
1629 return self.RpcServer().get_description(issue).strip()
1630 except urllib2.HTTPError as e:
1631 if e.code == 404:
1632 DieWithError(
1633 ('\nWhile fetching the description for issue %d, received a '
1634 '404 (not found)\n'
1635 'error. It is likely that you deleted this '
1636 'issue on the server. If this is the\n'
1637 'case, please run\n\n'
1638 ' git cl issue 0\n\n'
1639 'to clear the association with the deleted issue. Then run '
1640 'this command again.') % issue)
1641 else:
1642 DieWithError(
1643 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1644 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001645 print('Warning: Failed to retrieve CL description due to network '
1646 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001647 return ''
1648
1649 def GetMostRecentPatchset(self):
1650 return self.GetIssueProperties()['patchsets'][-1]
1651
1652 def GetPatchSetDiff(self, issue, patchset):
1653 return self.RpcServer().get(
1654 '/download/issue%s_%s.diff' % (issue, patchset))
1655
1656 def GetIssueProperties(self):
1657 if self._props is None:
1658 issue = self.GetIssue()
1659 if not issue:
1660 self._props = {}
1661 else:
1662 self._props = self.RpcServer().get_issue_properties(issue, True)
1663 return self._props
1664
1665 def GetApprovingReviewers(self):
1666 return get_approving_reviewers(self.GetIssueProperties())
1667
1668 def AddComment(self, message):
1669 return self.RpcServer().add_comment(self.GetIssue(), message)
1670
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001671 def GetStatus(self):
1672 """Apply a rough heuristic to give a simple summary of an issue's review
1673 or CQ status, assuming adherence to a common workflow.
1674
1675 Returns None if no issue for this branch, or one of the following keywords:
1676 * 'error' - error from review tool (including deleted issues)
1677 * 'unsent' - not sent for review
1678 * 'waiting' - waiting for review
1679 * 'reply' - waiting for owner to reply to review
1680 * 'lgtm' - LGTM from at least one approved reviewer
1681 * 'commit' - in the commit queue
1682 * 'closed' - closed
1683 """
1684 if not self.GetIssue():
1685 return None
1686
1687 try:
1688 props = self.GetIssueProperties()
1689 except urllib2.HTTPError:
1690 return 'error'
1691
1692 if props.get('closed'):
1693 # Issue is closed.
1694 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001695 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001696 # Issue is in the commit queue.
1697 return 'commit'
1698
1699 try:
1700 reviewers = self.GetApprovingReviewers()
1701 except urllib2.HTTPError:
1702 return 'error'
1703
1704 if reviewers:
1705 # Was LGTM'ed.
1706 return 'lgtm'
1707
1708 messages = props.get('messages') or []
1709
1710 if not messages:
1711 # No message was sent.
1712 return 'unsent'
1713 if messages[-1]['sender'] != props.get('owner_email'):
1714 # Non-LGTM reply from non-owner
1715 return 'reply'
1716 return 'waiting'
1717
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001718 def UpdateDescriptionRemote(self, description):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001719 return self.RpcServer().update_description(
1720 self.GetIssue(), self.description)
1721
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001722 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001723 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001724
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001725 def SetFlag(self, flag, value):
1726 """Patchset must match."""
1727 if not self.GetPatchset():
1728 DieWithError('The patchset needs to match. Send another patchset.')
1729 try:
1730 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001731 self.GetIssue(), self.GetPatchset(), flag, value)
vapierfd77ac72016-06-16 08:33:57 -07001732 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001733 if e.code == 404:
1734 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1735 if e.code == 403:
1736 DieWithError(
1737 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1738 'match?') % (self.GetIssue(), self.GetPatchset()))
1739 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001740
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001741 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001742 """Returns an upload.RpcServer() to access this review's rietveld instance.
1743 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001744 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001745 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001746 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001747 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001748 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001749
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001750 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001751 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001752 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001753
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001754 def PatchsetSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001755 """Return the git setting that stores this change's most recent patchset."""
1756 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1757
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001758 def GetCodereviewServerSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001759 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001760 branch = self.GetBranch()
1761 if branch:
1762 return 'branch.%s.rietveldserver' % branch
1763 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001764
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001765 def _PostUnsetIssueProperties(self):
1766 """Which branch-specific properties to erase when unsetting issue."""
1767 return ['rietveldserver']
1768
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001769 def GetRieveldObjForPresubmit(self):
1770 return self.RpcServer()
1771
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001772 def SetCQState(self, new_state):
1773 props = self.GetIssueProperties()
1774 if props.get('private'):
1775 DieWithError('Cannot set-commit on private issue')
1776
1777 if new_state == _CQState.COMMIT:
1778 self.SetFlag('commit', '1')
1779 elif new_state == _CQState.NONE:
1780 self.SetFlag('commit', '0')
1781 else:
1782 raise NotImplementedError()
1783
1784
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001785 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1786 directory):
1787 # TODO(maruel): Use apply_issue.py
1788
1789 # PatchIssue should never be called with a dirty tree. It is up to the
1790 # caller to check this, but just in case we assert here since the
1791 # consequences of the caller not checking this could be dire.
1792 assert(not git_common.is_dirty_git_tree('apply'))
1793 assert(parsed_issue_arg.valid)
1794 self._changelist.issue = parsed_issue_arg.issue
1795 if parsed_issue_arg.hostname:
1796 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1797
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001798 if (isinstance(parsed_issue_arg, _RietveldParsedIssueNumberArgument) and
1799 parsed_issue_arg.patch_url):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001800 assert parsed_issue_arg.patchset
1801 patchset = parsed_issue_arg.patchset
1802 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1803 else:
1804 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1805 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1806
1807 # Switch up to the top-level directory, if necessary, in preparation for
1808 # applying the patch.
1809 top = settings.GetRelativeRoot()
1810 if top:
1811 os.chdir(top)
1812
1813 # Git patches have a/ at the beginning of source paths. We strip that out
1814 # with a sed script rather than the -p flag to patch so we can feed either
1815 # Git or svn-style patches into the same apply command.
1816 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1817 try:
1818 patch_data = subprocess2.check_output(
1819 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1820 except subprocess2.CalledProcessError:
1821 DieWithError('Git patch mungling failed.')
1822 logging.info(patch_data)
1823
1824 # We use "git apply" to apply the patch instead of "patch" so that we can
1825 # pick up file adds.
1826 # The --index flag means: also insert into the index (so we catch adds).
1827 cmd = ['git', 'apply', '--index', '-p0']
1828 if directory:
1829 cmd.extend(('--directory', directory))
1830 if reject:
1831 cmd.append('--reject')
1832 elif IsGitVersionAtLeast('1.7.12'):
1833 cmd.append('--3way')
1834 try:
1835 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
1836 stdin=patch_data, stdout=subprocess2.VOID)
1837 except subprocess2.CalledProcessError:
vapiera7fbd5a2016-06-16 09:17:49 -07001838 print('Failed to apply the patch')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001839 return 1
1840
1841 # If we had an issue, commit the current state and register the issue.
1842 if not nocommit:
1843 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
1844 'patch from issue %(i)s at patchset '
1845 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
1846 % {'i': self.GetIssue(), 'p': patchset})])
1847 self.SetIssue(self.GetIssue())
1848 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07001849 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001850 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001851 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001852 return 0
1853
1854 @staticmethod
1855 def ParseIssueURL(parsed_url):
1856 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1857 return None
1858 # Typical url: https://domain/<issue_number>[/[other]]
1859 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
1860 if match:
1861 return _RietveldParsedIssueNumberArgument(
1862 issue=int(match.group(1)),
1863 hostname=parsed_url.netloc)
1864 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
1865 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
1866 if match:
1867 return _RietveldParsedIssueNumberArgument(
1868 issue=int(match.group(1)),
1869 patchset=int(match.group(2)),
1870 hostname=parsed_url.netloc,
1871 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
1872 return None
1873
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001874 def CMDUploadChange(self, options, args, change):
1875 """Upload the patch to Rietveld."""
1876 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1877 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001878 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
1879 if options.emulate_svn_auto_props:
1880 upload_args.append('--emulate_svn_auto_props')
1881
1882 change_desc = None
1883
1884 if options.email is not None:
1885 upload_args.extend(['--email', options.email])
1886
1887 if self.GetIssue():
1888 if options.title:
1889 upload_args.extend(['--title', options.title])
1890 if options.message:
1891 upload_args.extend(['--message', options.message])
1892 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07001893 print('This branch is associated with issue %s. '
1894 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001895 else:
1896 if options.title:
1897 upload_args.extend(['--title', options.title])
1898 message = (options.title or options.message or
1899 CreateDescriptionFromLog(args))
1900 change_desc = ChangeDescription(message)
1901 if options.reviewers or options.tbr_owners:
1902 change_desc.update_reviewers(options.reviewers,
1903 options.tbr_owners,
1904 change)
1905 if not options.force:
1906 change_desc.prompt()
1907
1908 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07001909 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001910 return 1
1911
1912 upload_args.extend(['--message', change_desc.description])
1913 if change_desc.get_reviewers():
1914 upload_args.append('--reviewers=%s' % ','.join(
1915 change_desc.get_reviewers()))
1916 if options.send_mail:
1917 if not change_desc.get_reviewers():
1918 DieWithError("Must specify reviewers to send email.")
1919 upload_args.append('--send_mail')
1920
1921 # We check this before applying rietveld.private assuming that in
1922 # rietveld.cc only addresses which we can send private CLs to are listed
1923 # if rietveld.private is set, and so we should ignore rietveld.cc only
1924 # when --private is specified explicitly on the command line.
1925 if options.private:
1926 logging.warn('rietveld.cc is ignored since private flag is specified. '
1927 'You need to review and add them manually if necessary.')
1928 cc = self.GetCCListWithoutDefault()
1929 else:
1930 cc = self.GetCCList()
1931 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
1932 if cc:
1933 upload_args.extend(['--cc', cc])
1934
1935 if options.private or settings.GetDefaultPrivateFlag() == "True":
1936 upload_args.append('--private')
1937
1938 upload_args.extend(['--git_similarity', str(options.similarity)])
1939 if not options.find_copies:
1940 upload_args.extend(['--git_no_find_copies'])
1941
1942 # Include the upstream repo's URL in the change -- this is useful for
1943 # projects that have their source spread across multiple repos.
1944 remote_url = self.GetGitBaseUrlFromConfig()
1945 if not remote_url:
1946 if settings.GetIsGitSvn():
1947 remote_url = self.GetGitSvnRemoteUrl()
1948 else:
1949 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
1950 remote_url = '%s@%s' % (self.GetRemoteUrl(),
1951 self.GetUpstreamBranch().split('/')[-1])
1952 if remote_url:
1953 upload_args.extend(['--base_url', remote_url])
1954 remote, remote_branch = self.GetRemoteBranch()
1955 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
1956 settings.GetPendingRefPrefix())
1957 if target_ref:
1958 upload_args.extend(['--target_ref', target_ref])
1959
1960 # Look for dependent patchsets. See crbug.com/480453 for more details.
1961 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
1962 upstream_branch = ShortBranchName(upstream_branch)
1963 if remote is '.':
1964 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001965 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001966 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07001967 print()
1968 print('Skipping dependency patchset upload because git config '
1969 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
1970 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001971 else:
1972 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001973 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001974 auth_config=auth_config)
1975 branch_cl_issue_url = branch_cl.GetIssueURL()
1976 branch_cl_issue = branch_cl.GetIssue()
1977 branch_cl_patchset = branch_cl.GetPatchset()
1978 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
1979 upload_args.extend(
1980 ['--depends_on_patchset', '%s:%s' % (
1981 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001982 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001983 '\n'
1984 'The current branch (%s) is tracking a local branch (%s) with '
1985 'an associated CL.\n'
1986 'Adding %s/#ps%s as a dependency patchset.\n'
1987 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
1988 branch_cl_patchset))
1989
1990 project = settings.GetProject()
1991 if project:
1992 upload_args.extend(['--project', project])
1993
1994 if options.cq_dry_run:
1995 upload_args.extend(['--cq_dry_run'])
1996
1997 try:
1998 upload_args = ['upload'] + upload_args + args
1999 logging.info('upload.RealMain(%s)', upload_args)
2000 issue, patchset = upload.RealMain(upload_args)
2001 issue = int(issue)
2002 patchset = int(patchset)
2003 except KeyboardInterrupt:
2004 sys.exit(1)
2005 except:
2006 # If we got an exception after the user typed a description for their
2007 # change, back up the description before re-raising.
2008 if change_desc:
2009 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2010 print('\nGot exception while uploading -- saving description to %s\n' %
2011 backup_path)
2012 backup_file = open(backup_path, 'w')
2013 backup_file.write(change_desc.description)
2014 backup_file.close()
2015 raise
2016
2017 if not self.GetIssue():
2018 self.SetIssue(issue)
2019 self.SetPatchset(patchset)
2020
2021 if options.use_commit_queue:
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002022 self.SetCQState(_CQState.COMMIT)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002023 return 0
2024
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002025
2026class _GerritChangelistImpl(_ChangelistCodereviewBase):
2027 def __init__(self, changelist, auth_config=None):
2028 # auth_config is Rietveld thing, kept here to preserve interface only.
2029 super(_GerritChangelistImpl, self).__init__(changelist)
2030 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002031 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002032 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002033 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002034
2035 def _GetGerritHost(self):
2036 # Lazy load of configs.
2037 self.GetCodereviewServer()
2038 return self._gerrit_host
2039
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002040 def _GetGitHost(self):
2041 """Returns git host to be used when uploading change to Gerrit."""
2042 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2043
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002044 def GetCodereviewServer(self):
2045 if not self._gerrit_server:
2046 # If we're on a branch then get the server potentially associated
2047 # with that branch.
2048 if self.GetIssue():
2049 gerrit_server_setting = self.GetCodereviewServerSetting()
2050 if gerrit_server_setting:
2051 self._gerrit_server = RunGit(['config', gerrit_server_setting],
2052 error_ok=True).strip()
2053 if self._gerrit_server:
2054 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
2055 if not self._gerrit_server:
2056 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2057 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002058 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002059 parts[0] = parts[0] + '-review'
2060 self._gerrit_host = '.'.join(parts)
2061 self._gerrit_server = 'https://%s' % self._gerrit_host
2062 return self._gerrit_server
2063
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002064 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00002065 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002066 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002067
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002068 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002069 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002070 if settings.GetGerritSkipEnsureAuthenticated():
2071 # For projects with unusual authentication schemes.
2072 # See http://crbug.com/603378.
2073 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002074 # Lazy-loader to identify Gerrit and Git hosts.
2075 if gerrit_util.GceAuthenticator.is_gce():
2076 return
2077 self.GetCodereviewServer()
2078 git_host = self._GetGitHost()
2079 assert self._gerrit_server and self._gerrit_host
2080 cookie_auth = gerrit_util.CookiesAuthenticator()
2081
2082 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2083 git_auth = cookie_auth.get_auth_header(git_host)
2084 if gerrit_auth and git_auth:
2085 if gerrit_auth == git_auth:
2086 return
2087 print((
2088 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2089 ' Check your %s or %s file for credentials of hosts:\n'
2090 ' %s\n'
2091 ' %s\n'
2092 ' %s') %
2093 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2094 git_host, self._gerrit_host,
2095 cookie_auth.get_new_password_message(git_host)))
2096 if not force:
2097 ask_for_data('If you know what you are doing, press Enter to continue, '
2098 'Ctrl+C to abort.')
2099 return
2100 else:
2101 missing = (
2102 [] if gerrit_auth else [self._gerrit_host] +
2103 [] if git_auth else [git_host])
2104 DieWithError('Credentials for the following hosts are required:\n'
2105 ' %s\n'
2106 'These are read from %s (or legacy %s)\n'
2107 '%s' % (
2108 '\n '.join(missing),
2109 cookie_auth.get_gitcookies_path(),
2110 cookie_auth.get_netrc_path(),
2111 cookie_auth.get_new_password_message(git_host)))
2112
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002113
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002114 def PatchsetSetting(self):
2115 """Return the git setting that stores this change's most recent patchset."""
2116 return 'branch.%s.gerritpatchset' % self.GetBranch()
2117
2118 def GetCodereviewServerSetting(self):
2119 """Returns the git setting that stores this change's Gerrit server."""
2120 branch = self.GetBranch()
2121 if branch:
2122 return 'branch.%s.gerritserver' % branch
2123 return None
2124
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002125 def _PostUnsetIssueProperties(self):
2126 """Which branch-specific properties to erase when unsetting issue."""
2127 return [
2128 'gerritserver',
2129 'gerritsquashhash',
2130 ]
2131
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002132 def GetRieveldObjForPresubmit(self):
2133 class ThisIsNotRietveldIssue(object):
2134 def __nonzero__(self):
2135 # This is a hack to make presubmit_support think that rietveld is not
2136 # defined, yet still ensure that calls directly result in a decent
2137 # exception message below.
2138 return False
2139
2140 def __getattr__(self, attr):
2141 print(
2142 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2143 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2144 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2145 'or use Rietveld for codereview.\n'
2146 'See also http://crbug.com/579160.' % attr)
2147 raise NotImplementedError()
2148 return ThisIsNotRietveldIssue()
2149
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002150 def GetGerritObjForPresubmit(self):
2151 return presubmit_support.GerritAccessor(self._GetGerritHost())
2152
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002153 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002154 """Apply a rough heuristic to give a simple summary of an issue's review
2155 or CQ status, assuming adherence to a common workflow.
2156
2157 Returns None if no issue for this branch, or one of the following keywords:
2158 * 'error' - error from review tool (including deleted issues)
2159 * 'unsent' - no reviewers added
2160 * 'waiting' - waiting for review
2161 * 'reply' - waiting for owner to reply to review
2162 * 'not lgtm' - Code-Review -2 from at least one approved reviewer
2163 * 'lgtm' - Code-Review +2 from at least one approved reviewer
2164 * 'commit' - in the commit queue
2165 * 'closed' - abandoned
2166 """
2167 if not self.GetIssue():
2168 return None
2169
2170 try:
2171 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
2172 except httplib.HTTPException:
2173 return 'error'
2174
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002175 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002176 return 'closed'
2177
2178 cq_label = data['labels'].get('Commit-Queue', {})
2179 if cq_label:
2180 # Vote value is a stringified integer, which we expect from 0 to 2.
2181 vote_value = cq_label.get('value', '0')
2182 vote_text = cq_label.get('values', {}).get(vote_value, '')
2183 if vote_text.lower() == 'commit':
2184 return 'commit'
2185
2186 lgtm_label = data['labels'].get('Code-Review', {})
2187 if lgtm_label:
2188 if 'rejected' in lgtm_label:
2189 return 'not lgtm'
2190 if 'approved' in lgtm_label:
2191 return 'lgtm'
2192
2193 if not data.get('reviewers', {}).get('REVIEWER', []):
2194 return 'unsent'
2195
2196 messages = data.get('messages', [])
2197 if messages:
2198 owner = data['owner'].get('_account_id')
2199 last_message_author = messages[-1].get('author', {}).get('_account_id')
2200 if owner != last_message_author:
2201 # Some reply from non-owner.
2202 return 'reply'
2203
2204 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002205
2206 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002207 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002208 return data['revisions'][data['current_revision']]['_number']
2209
2210 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002211 data = self._GetChangeDetail(['CURRENT_REVISION'])
2212 current_rev = data['current_revision']
2213 url = data['revisions'][current_rev]['fetch']['http']['url']
2214 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002215
2216 def UpdateDescriptionRemote(self, description):
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002217 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
2218 description)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002219
2220 def CloseIssue(self):
2221 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2222
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002223 def GetApprovingReviewers(self):
2224 """Returns a list of reviewers approving the change.
2225
2226 Note: not necessarily committers.
2227 """
2228 raise NotImplementedError()
2229
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002230 def SubmitIssue(self, wait_for_merge=True):
2231 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2232 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002233
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002234 def _GetChangeDetail(self, options=None, issue=None):
2235 options = options or []
2236 issue = issue or self.GetIssue()
2237 assert issue, 'issue required to query Gerrit'
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002238 return gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2239 options)
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002240
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002241 def CMDLand(self, force, bypass_hooks, verbose):
2242 if git_common.is_dirty_git_tree('land'):
2243 return 1
2244 differs = True
2245 last_upload = RunGit(['config',
2246 'branch.%s.gerritsquashhash' % self.GetBranch()],
2247 error_ok=True).strip()
2248 # Note: git diff outputs nothing if there is no diff.
2249 if not last_upload or RunGit(['diff', last_upload]).strip():
2250 print('WARNING: some changes from local branch haven\'t been uploaded')
2251 else:
2252 detail = self._GetChangeDetail(['CURRENT_REVISION'])
2253 if detail['current_revision'] == last_upload:
2254 differs = False
2255 else:
2256 print('WARNING: local branch contents differ from latest uploaded '
2257 'patchset')
2258 if differs:
2259 if not force:
2260 ask_for_data(
2261 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2262 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2263 elif not bypass_hooks:
2264 hook_results = self.RunHook(
2265 committing=True,
2266 may_prompt=not force,
2267 verbose=verbose,
2268 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2269 if not hook_results.should_continue():
2270 return 1
2271
2272 self.SubmitIssue(wait_for_merge=True)
2273 print('Issue %s has been submitted.' % self.GetIssueURL())
2274 return 0
2275
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002276 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2277 directory):
2278 assert not reject
2279 assert not nocommit
2280 assert not directory
2281 assert parsed_issue_arg.valid
2282
2283 self._changelist.issue = parsed_issue_arg.issue
2284
2285 if parsed_issue_arg.hostname:
2286 self._gerrit_host = parsed_issue_arg.hostname
2287 self._gerrit_server = 'https://%s' % self._gerrit_host
2288
2289 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2290
2291 if not parsed_issue_arg.patchset:
2292 # Use current revision by default.
2293 revision_info = detail['revisions'][detail['current_revision']]
2294 patchset = int(revision_info['_number'])
2295 else:
2296 patchset = parsed_issue_arg.patchset
2297 for revision_info in detail['revisions'].itervalues():
2298 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2299 break
2300 else:
2301 DieWithError('Couldn\'t find patchset %i in issue %i' %
2302 (parsed_issue_arg.patchset, self.GetIssue()))
2303
2304 fetch_info = revision_info['fetch']['http']
2305 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2306 RunGit(['cherry-pick', 'FETCH_HEAD'])
2307 self.SetIssue(self.GetIssue())
2308 self.SetPatchset(patchset)
2309 print('Committed patch for issue %i pathset %i locally' %
2310 (self.GetIssue(), self.GetPatchset()))
2311 return 0
2312
2313 @staticmethod
2314 def ParseIssueURL(parsed_url):
2315 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2316 return None
2317 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2318 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2319 # Short urls like https://domain/<issue_number> can be used, but don't allow
2320 # specifying the patchset (you'd 404), but we allow that here.
2321 if parsed_url.path == '/':
2322 part = parsed_url.fragment
2323 else:
2324 part = parsed_url.path
2325 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2326 if match:
2327 return _ParsedIssueNumberArgument(
2328 issue=int(match.group(2)),
2329 patchset=int(match.group(4)) if match.group(4) else None,
2330 hostname=parsed_url.netloc)
2331 return None
2332
tandrii16e0b4e2016-06-07 10:34:28 -07002333 def _GerritCommitMsgHookCheck(self, offer_removal):
2334 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2335 if not os.path.exists(hook):
2336 return
2337 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2338 # custom developer made one.
2339 data = gclient_utils.FileRead(hook)
2340 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2341 return
2342 print('Warning: you have Gerrit commit-msg hook installed.\n'
2343 'It is not neccessary for uploading with git cl in squash mode, '
2344 'and may interfere with it in subtle ways.\n'
2345 'We recommend you remove the commit-msg hook.')
2346 if offer_removal:
2347 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2348 if reply.lower().startswith('y'):
2349 gclient_utils.rm_file_or_tree(hook)
2350 print('Gerrit commit-msg hook removed.')
2351 else:
2352 print('OK, will keep Gerrit commit-msg hook in place.')
2353
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002354 def CMDUploadChange(self, options, args, change):
2355 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002356 if options.squash and options.no_squash:
2357 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002358
2359 if not options.squash and not options.no_squash:
2360 # Load default for user, repo, squash=true, in this order.
2361 options.squash = settings.GetSquashGerritUploads()
2362 elif options.no_squash:
2363 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002364
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002365 # We assume the remote called "origin" is the one we want.
2366 # It is probably not worthwhile to support different workflows.
2367 gerrit_remote = 'origin'
2368
2369 remote, remote_branch = self.GetRemoteBranch()
2370 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2371 pending_prefix='')
2372
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002373 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002374 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002375 if not self.GetIssue():
2376 # TODO(tandrii): deperecate this after 2016Q2. Backwards compatibility
2377 # with shadow branch, which used to contain change-id for a given
2378 # branch, using which we can fetch actual issue number and set it as the
2379 # property of the branch, which is the new way.
2380 message = RunGitSilent([
2381 'show', '--format=%B', '-s',
2382 'refs/heads/git_cl_uploads/%s' % self.GetBranch()])
2383 if message:
2384 change_ids = git_footers.get_footer_change_id(message.strip())
2385 if change_ids and len(change_ids) == 1:
2386 details = self._GetChangeDetail(issue=change_ids[0])
2387 if details:
2388 print('WARNING: found old upload in branch git_cl_uploads/%s '
2389 'corresponding to issue %s' %
2390 (self.GetBranch(), details['_number']))
2391 self.SetIssue(details['_number'])
2392 if not self.GetIssue():
2393 DieWithError(
2394 '\n' # For readability of the blob below.
2395 'Found old upload in branch git_cl_uploads/%s, '
2396 'but failed to find corresponding Gerrit issue.\n'
2397 'If you know the issue number, set it manually first:\n'
2398 ' git cl issue 123456\n'
2399 'If you intended to upload this CL as new issue, '
2400 'just delete or rename the old upload branch:\n'
2401 ' git rename-branch git_cl_uploads/%s old_upload-%s\n'
2402 'After that, please run git cl upload again.' %
2403 tuple([self.GetBranch()] * 3))
2404 # End of backwards compatability.
2405
2406 if self.GetIssue():
2407 # Try to get the message from a previous upload.
2408 message = self.GetDescription()
2409 if not message:
2410 DieWithError(
2411 'failed to fetch description from current Gerrit issue %d\n'
2412 '%s' % (self.GetIssue(), self.GetIssueURL()))
2413 change_id = self._GetChangeDetail()['change_id']
2414 while True:
2415 footer_change_ids = git_footers.get_footer_change_id(message)
2416 if footer_change_ids == [change_id]:
2417 break
2418 if not footer_change_ids:
2419 message = git_footers.add_footer_change_id(message, change_id)
2420 print('WARNING: appended missing Change-Id to issue description')
2421 continue
2422 # There is already a valid footer but with different or several ids.
2423 # Doing this automatically is non-trivial as we don't want to lose
2424 # existing other footers, yet we want to append just 1 desired
2425 # Change-Id. Thus, just create a new footer, but let user verify the
2426 # new description.
2427 message = '%s\n\nChange-Id: %s' % (message, change_id)
2428 print(
2429 'WARNING: issue %s has Change-Id footer(s):\n'
2430 ' %s\n'
2431 'but issue has Change-Id %s, according to Gerrit.\n'
2432 'Please, check the proposed correction to the description, '
2433 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2434 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2435 change_id))
2436 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2437 if not options.force:
2438 change_desc = ChangeDescription(message)
2439 change_desc.prompt()
2440 message = change_desc.description
2441 if not message:
2442 DieWithError("Description is empty. Aborting...")
2443 # Continue the while loop.
2444 # Sanity check of this code - we should end up with proper message
2445 # footer.
2446 assert [change_id] == git_footers.get_footer_change_id(message)
2447 change_desc = ChangeDescription(message)
2448 else:
2449 change_desc = ChangeDescription(
2450 options.message or CreateDescriptionFromLog(args))
2451 if not options.force:
2452 change_desc.prompt()
2453 if not change_desc.description:
2454 DieWithError("Description is empty. Aborting...")
2455 message = change_desc.description
2456 change_ids = git_footers.get_footer_change_id(message)
2457 if len(change_ids) > 1:
2458 DieWithError('too many Change-Id footers, at most 1 allowed.')
2459 if not change_ids:
2460 # Generate the Change-Id automatically.
2461 message = git_footers.add_footer_change_id(
2462 message, GenerateGerritChangeId(message))
2463 change_desc.set_description(message)
2464 change_ids = git_footers.get_footer_change_id(message)
2465 assert len(change_ids) == 1
2466 change_id = change_ids[0]
2467
2468 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2469 if remote is '.':
2470 # If our upstream branch is local, we base our squashed commit on its
2471 # squashed version.
2472 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2473 # Check the squashed hash of the parent.
2474 parent = RunGit(['config',
2475 'branch.%s.gerritsquashhash' % upstream_branch_name],
2476 error_ok=True).strip()
2477 # Verify that the upstream branch has been uploaded too, otherwise
2478 # Gerrit will create additional CLs when uploading.
2479 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2480 RunGitSilent(['rev-parse', parent + ':'])):
2481 # TODO(tandrii): remove "old depot_tools" part on April 12, 2016.
2482 DieWithError(
2483 'Upload upstream branch %s first.\n'
2484 'Note: maybe you\'ve uploaded it with --no-squash or with an old '
2485 'version of depot_tools. If so, then re-upload it with:\n'
2486 ' git cl upload --squash\n' % upstream_branch_name)
2487 else:
2488 parent = self.GetCommonAncestorWithUpstream()
2489
2490 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2491 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2492 '-m', message]).strip()
2493 else:
2494 change_desc = ChangeDescription(
2495 options.message or CreateDescriptionFromLog(args))
2496 if not change_desc.description:
2497 DieWithError("Description is empty. Aborting...")
2498
2499 if not git_footers.get_footer_change_id(change_desc.description):
2500 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002501 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2502 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002503 ref_to_push = 'HEAD'
2504 parent = '%s/%s' % (gerrit_remote, branch)
2505 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2506
2507 assert change_desc
2508 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2509 ref_to_push)]).splitlines()
2510 if len(commits) > 1:
2511 print('WARNING: This will upload %d commits. Run the following command '
2512 'to see which commits will be uploaded: ' % len(commits))
2513 print('git log %s..%s' % (parent, ref_to_push))
2514 print('You can also use `git squash-branch` to squash these into a '
2515 'single commit.')
2516 ask_for_data('About to upload; enter to confirm.')
2517
2518 if options.reviewers or options.tbr_owners:
2519 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2520 change)
2521
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002522 # Extra options that can be specified at push time. Doc:
2523 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2524 refspec_opts = []
2525 if options.title:
2526 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2527 # reverse on its side.
2528 if '_' in options.title:
2529 print('WARNING: underscores in title will be converted to spaces.')
2530 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2531
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002532 if options.send_mail:
2533 if not change_desc.get_reviewers():
2534 DieWithError('Must specify reviewers to send email.')
2535 refspec_opts.append('notify=ALL')
2536 else:
2537 refspec_opts.append('notify=NONE')
2538
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002539 cc = self.GetCCList().split(',')
2540 if options.cc:
2541 cc.extend(options.cc)
2542 cc = filter(None, cc)
2543 if cc:
tandrii@chromium.org074c2af2016-06-03 23:18:40 +00002544 refspec_opts.extend('cc=' + email.strip() for email in cc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002545
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002546 if change_desc.get_reviewers():
2547 refspec_opts.extend('r=' + email.strip()
2548 for email in change_desc.get_reviewers())
2549
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002550 refspec_suffix = ''
2551 if refspec_opts:
2552 refspec_suffix = '%' + ','.join(refspec_opts)
2553 assert ' ' not in refspec_suffix, (
2554 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002555 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002556
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002557 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002558 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002559 print_stdout=True,
2560 # Flush after every line: useful for seeing progress when running as
2561 # recipe.
2562 filter_fn=lambda _: sys.stdout.flush())
2563
2564 if options.squash:
2565 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2566 change_numbers = [m.group(1)
2567 for m in map(regex.match, push_stdout.splitlines())
2568 if m]
2569 if len(change_numbers) != 1:
2570 DieWithError(
2571 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2572 'Change-Id: %s') % (len(change_numbers), change_id))
2573 self.SetIssue(change_numbers[0])
2574 RunGit(['config', 'branch.%s.gerritsquashhash' % self.GetBranch(),
2575 ref_to_push])
2576 return 0
2577
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002578 def _AddChangeIdToCommitMessage(self, options, args):
2579 """Re-commits using the current message, assumes the commit hook is in
2580 place.
2581 """
2582 log_desc = options.message or CreateDescriptionFromLog(args)
2583 git_command = ['commit', '--amend', '-m', log_desc]
2584 RunGit(git_command)
2585 new_log_desc = CreateDescriptionFromLog(args)
2586 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002587 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002588 return new_log_desc
2589 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002590 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002591
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002592 def SetCQState(self, new_state):
2593 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
2594 # TODO(tandrii): maybe allow configurability in codereview.settings or by
2595 # self-discovery of label config for this CL using REST API.
2596 vote_map = {
2597 _CQState.NONE: 0,
2598 _CQState.DRY_RUN: 1,
2599 _CQState.COMMIT : 2,
2600 }
2601 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2602 labels={'Commit-Queue': vote_map[new_state]})
2603
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002604
2605_CODEREVIEW_IMPLEMENTATIONS = {
2606 'rietveld': _RietveldChangelistImpl,
2607 'gerrit': _GerritChangelistImpl,
2608}
2609
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002610
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002611def _add_codereview_select_options(parser):
2612 """Appends --gerrit and --rietveld options to force specific codereview."""
2613 parser.codereview_group = optparse.OptionGroup(
2614 parser, 'EXPERIMENTAL! Codereview override options')
2615 parser.add_option_group(parser.codereview_group)
2616 parser.codereview_group.add_option(
2617 '--gerrit', action='store_true',
2618 help='Force the use of Gerrit for codereview')
2619 parser.codereview_group.add_option(
2620 '--rietveld', action='store_true',
2621 help='Force the use of Rietveld for codereview')
2622
2623
2624def _process_codereview_select_options(parser, options):
2625 if options.gerrit and options.rietveld:
2626 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2627 options.forced_codereview = None
2628 if options.gerrit:
2629 options.forced_codereview = 'gerrit'
2630 elif options.rietveld:
2631 options.forced_codereview = 'rietveld'
2632
2633
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002634class ChangeDescription(object):
2635 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002636 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002637 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002638
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002639 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002640 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002641
agable@chromium.org42c20792013-09-12 17:34:49 +00002642 @property # www.logilab.org/ticket/89786
2643 def description(self): # pylint: disable=E0202
2644 return '\n'.join(self._description_lines)
2645
2646 def set_description(self, desc):
2647 if isinstance(desc, basestring):
2648 lines = desc.splitlines()
2649 else:
2650 lines = [line.rstrip() for line in desc]
2651 while lines and not lines[0]:
2652 lines.pop(0)
2653 while lines and not lines[-1]:
2654 lines.pop(-1)
2655 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002656
piman@chromium.org336f9122014-09-04 02:16:55 +00002657 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002658 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002659 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002660 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002661 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002662 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002663
agable@chromium.org42c20792013-09-12 17:34:49 +00002664 # Get the set of R= and TBR= lines and remove them from the desciption.
2665 regexp = re.compile(self.R_LINE)
2666 matches = [regexp.match(line) for line in self._description_lines]
2667 new_desc = [l for i, l in enumerate(self._description_lines)
2668 if not matches[i]]
2669 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002670
agable@chromium.org42c20792013-09-12 17:34:49 +00002671 # Construct new unified R= and TBR= lines.
2672 r_names = []
2673 tbr_names = []
2674 for match in matches:
2675 if not match:
2676 continue
2677 people = cleanup_list([match.group(2).strip()])
2678 if match.group(1) == 'TBR':
2679 tbr_names.extend(people)
2680 else:
2681 r_names.extend(people)
2682 for name in r_names:
2683 if name not in reviewers:
2684 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002685 if add_owners_tbr:
2686 owners_db = owners.Database(change.RepositoryRoot(),
2687 fopen=file, os_path=os.path, glob=glob.glob)
2688 all_reviewers = set(tbr_names + reviewers)
2689 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2690 all_reviewers)
2691 tbr_names.extend(owners_db.reviewers_for(missing_files,
2692 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002693 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2694 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2695
2696 # Put the new lines in the description where the old first R= line was.
2697 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2698 if 0 <= line_loc < len(self._description_lines):
2699 if new_tbr_line:
2700 self._description_lines.insert(line_loc, new_tbr_line)
2701 if new_r_line:
2702 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002703 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002704 if new_r_line:
2705 self.append_footer(new_r_line)
2706 if new_tbr_line:
2707 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002708
2709 def prompt(self):
2710 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002711 self.set_description([
2712 '# Enter a description of the change.',
2713 '# This will be displayed on the codereview site.',
2714 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002715 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002716 '--------------------',
2717 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002718
agable@chromium.org42c20792013-09-12 17:34:49 +00002719 regexp = re.compile(self.BUG_LINE)
2720 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00002721 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00002722 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002723 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002724 if not content:
2725 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002726 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002727
2728 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002729 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2730 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002731 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002732 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002733
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002734 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00002735 """Adds a footer line to the description.
2736
2737 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
2738 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
2739 that Gerrit footers are always at the end.
2740 """
2741 parsed_footer_line = git_footers.parse_footer(line)
2742 if parsed_footer_line:
2743 # Line is a gerrit footer in the form: Footer-Key: any value.
2744 # Thus, must be appended observing Gerrit footer rules.
2745 self.set_description(
2746 git_footers.add_footer(self.description,
2747 key=parsed_footer_line[0],
2748 value=parsed_footer_line[1]))
2749 return
2750
2751 if not self._description_lines:
2752 self._description_lines.append(line)
2753 return
2754
2755 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
2756 if gerrit_footers:
2757 # git_footers.split_footers ensures that there is an empty line before
2758 # actual (gerrit) footers, if any. We have to keep it that way.
2759 assert top_lines and top_lines[-1] == ''
2760 top_lines, separator = top_lines[:-1], top_lines[-1:]
2761 else:
2762 separator = [] # No need for separator if there are no gerrit_footers.
2763
2764 prev_line = top_lines[-1] if top_lines else ''
2765 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
2766 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2767 top_lines.append('')
2768 top_lines.append(line)
2769 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002770
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002771 def get_reviewers(self):
2772 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002773 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
2774 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002775 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002776
2777
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002778def get_approving_reviewers(props):
2779 """Retrieves the reviewers that approved a CL from the issue properties with
2780 messages.
2781
2782 Note that the list may contain reviewers that are not committer, thus are not
2783 considered by the CQ.
2784 """
2785 return sorted(
2786 set(
2787 message['sender']
2788 for message in props['messages']
2789 if message['approval'] and message['sender'] in props['reviewers']
2790 )
2791 )
2792
2793
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002794def FindCodereviewSettingsFile(filename='codereview.settings'):
2795 """Finds the given file starting in the cwd and going up.
2796
2797 Only looks up to the top of the repository unless an
2798 'inherit-review-settings-ok' file exists in the root of the repository.
2799 """
2800 inherit_ok_file = 'inherit-review-settings-ok'
2801 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002802 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002803 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2804 root = '/'
2805 while True:
2806 if filename in os.listdir(cwd):
2807 if os.path.isfile(os.path.join(cwd, filename)):
2808 return open(os.path.join(cwd, filename))
2809 if cwd == root:
2810 break
2811 cwd = os.path.dirname(cwd)
2812
2813
2814def LoadCodereviewSettingsFromFile(fileobj):
2815 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002816 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002817
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002818 def SetProperty(name, setting, unset_error_ok=False):
2819 fullname = 'rietveld.' + name
2820 if setting in keyvals:
2821 RunGit(['config', fullname, keyvals[setting]])
2822 else:
2823 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2824
2825 SetProperty('server', 'CODE_REVIEW_SERVER')
2826 # Only server setting is required. Other settings can be absent.
2827 # In that case, we ignore errors raised during option deletion attempt.
2828 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002829 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002830 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2831 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002832 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002833 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002834 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2835 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002836 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002837 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002838 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002839 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2840 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002841
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002842 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002843 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002844
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002845 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07002846 RunGit(['config', 'gerrit.squash-uploads',
2847 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002848
tandrii@chromium.org28253532016-04-14 13:46:56 +00002849 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00002850 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00002851 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
2852
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002853 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2854 #should be of the form
2855 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2856 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2857 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2858 keyvals['ORIGIN_URL_CONFIG']])
2859
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002860
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002861def urlretrieve(source, destination):
2862 """urllib is broken for SSL connections via a proxy therefore we
2863 can't use urllib.urlretrieve()."""
2864 with open(destination, 'w') as f:
2865 f.write(urllib2.urlopen(source).read())
2866
2867
ukai@chromium.org712d6102013-11-27 00:52:58 +00002868def hasSheBang(fname):
2869 """Checks fname is a #! script."""
2870 with open(fname) as f:
2871 return f.read(2).startswith('#!')
2872
2873
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00002874# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
2875def DownloadHooks(*args, **kwargs):
2876 pass
2877
2878
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002879def DownloadGerritHook(force):
2880 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002881
2882 Args:
2883 force: True to update hooks. False to install hooks if not present.
2884 """
2885 if not settings.GetIsGerrit():
2886 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00002887 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002888 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2889 if not os.access(dst, os.X_OK):
2890 if os.path.exists(dst):
2891 if not force:
2892 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002893 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002894 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002895 if not hasSheBang(dst):
2896 DieWithError('Not a script: %s\n'
2897 'You need to download from\n%s\n'
2898 'into .git/hooks/commit-msg and '
2899 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002900 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2901 except Exception:
2902 if os.path.exists(dst):
2903 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002904 DieWithError('\nFailed to download hooks.\n'
2905 'You need to download from\n%s\n'
2906 'into .git/hooks/commit-msg and '
2907 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002908
2909
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002910
2911def GetRietveldCodereviewSettingsInteractively():
2912 """Prompt the user for settings."""
2913 server = settings.GetDefaultServerUrl(error_ok=True)
2914 prompt = 'Rietveld server (host[:port])'
2915 prompt += ' [%s]' % (server or DEFAULT_SERVER)
2916 newserver = ask_for_data(prompt + ':')
2917 if not server and not newserver:
2918 newserver = DEFAULT_SERVER
2919 if newserver:
2920 newserver = gclient_utils.UpgradeToHttps(newserver)
2921 if newserver != server:
2922 RunGit(['config', 'rietveld.server', newserver])
2923
2924 def SetProperty(initial, caption, name, is_url):
2925 prompt = caption
2926 if initial:
2927 prompt += ' ("x" to clear) [%s]' % initial
2928 new_val = ask_for_data(prompt + ':')
2929 if new_val == 'x':
2930 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
2931 elif new_val:
2932 if is_url:
2933 new_val = gclient_utils.UpgradeToHttps(new_val)
2934 if new_val != initial:
2935 RunGit(['config', 'rietveld.' + name, new_val])
2936
2937 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
2938 SetProperty(settings.GetDefaultPrivateFlag(),
2939 'Private flag (rietveld only)', 'private', False)
2940 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
2941 'tree-status-url', False)
2942 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
2943 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
2944 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
2945 'run-post-upload-hook', False)
2946
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002947@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002948def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002949 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002950
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002951 print('WARNING: git cl config works for Rietveld only.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002952 'For Gerrit, see http://crbug.com/603116.')
2953 # TODO(tandrii): add Gerrit support as part of http://crbug.com/603116.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00002954 parser.add_option('--activate-update', action='store_true',
2955 help='activate auto-updating [rietveld] section in '
2956 '.git/config')
2957 parser.add_option('--deactivate-update', action='store_true',
2958 help='deactivate auto-updating [rietveld] section in '
2959 '.git/config')
2960 options, args = parser.parse_args(args)
2961
2962 if options.deactivate_update:
2963 RunGit(['config', 'rietveld.autoupdate', 'false'])
2964 return
2965
2966 if options.activate_update:
2967 RunGit(['config', '--unset', 'rietveld.autoupdate'])
2968 return
2969
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002970 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002971 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002972 return 0
2973
2974 url = args[0]
2975 if not url.endswith('codereview.settings'):
2976 url = os.path.join(url, 'codereview.settings')
2977
2978 # Load code review settings and download hooks (if available).
2979 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
2980 return 0
2981
2982
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002983def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002984 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002985 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
2986 branch = ShortBranchName(branchref)
2987 _, args = parser.parse_args(args)
2988 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07002989 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002990 return RunGit(['config', 'branch.%s.base-url' % branch],
2991 error_ok=False).strip()
2992 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002993 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002994 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
2995 error_ok=False).strip()
2996
2997
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002998def color_for_status(status):
2999 """Maps a Changelist status to color, for CMDstatus and other tools."""
3000 return {
3001 'unsent': Fore.RED,
3002 'waiting': Fore.BLUE,
3003 'reply': Fore.YELLOW,
3004 'lgtm': Fore.GREEN,
3005 'commit': Fore.MAGENTA,
3006 'closed': Fore.CYAN,
3007 'error': Fore.WHITE,
3008 }.get(status, Fore.WHITE)
3009
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003010
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003011def get_cl_statuses(changes, fine_grained, max_processes=None):
3012 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003013
3014 If fine_grained is true, this will fetch CL statuses from the server.
3015 Otherwise, simply indicate if there's a matching url for the given branches.
3016
3017 If max_processes is specified, it is used as the maximum number of processes
3018 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3019 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003020
3021 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003022 """
3023 # Silence upload.py otherwise it becomes unwieldly.
3024 upload.verbosity = 0
3025
3026 if fine_grained:
3027 # Process one branch synchronously to work through authentication, then
3028 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003029 if changes:
3030 fetch = lambda cl: (cl, cl.GetStatus())
3031 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003032
kmarshall3bff56b2016-06-06 18:31:47 -07003033 if not changes:
3034 # Exit early if there was only one branch to fetch.
3035 return
3036
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003037 changes_to_fetch = changes[1:]
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003038 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003039 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003040 if max_processes is not None
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003041 else len(changes_to_fetch))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003042
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003043 fetched_cls = set()
3044 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003045 while True:
3046 try:
3047 row = it.next(timeout=5)
3048 except multiprocessing.TimeoutError:
3049 break
3050
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003051 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003052 yield row
3053
3054 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003055 for cl in set(changes_to_fetch) - fetched_cls:
3056 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003057
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003058 else:
3059 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003060 for cl in changes:
3061 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003062
rmistry@google.com2dd99862015-06-22 12:22:18 +00003063
3064def upload_branch_deps(cl, args):
3065 """Uploads CLs of local branches that are dependents of the current branch.
3066
3067 If the local branch dependency tree looks like:
3068 test1 -> test2.1 -> test3.1
3069 -> test3.2
3070 -> test2.2 -> test3.3
3071
3072 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3073 run on the dependent branches in this order:
3074 test2.1, test3.1, test3.2, test2.2, test3.3
3075
3076 Note: This function does not rebase your local dependent branches. Use it when
3077 you make a change to the parent branch that will not conflict with its
3078 dependent branches, and you would like their dependencies updated in
3079 Rietveld.
3080 """
3081 if git_common.is_dirty_git_tree('upload-branch-deps'):
3082 return 1
3083
3084 root_branch = cl.GetBranch()
3085 if root_branch is None:
3086 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3087 'Get on a branch!')
3088 if not cl.GetIssue() or not cl.GetPatchset():
3089 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3090 'patchset dependencies without an uploaded CL.')
3091
3092 branches = RunGit(['for-each-ref',
3093 '--format=%(refname:short) %(upstream:short)',
3094 'refs/heads'])
3095 if not branches:
3096 print('No local branches found.')
3097 return 0
3098
3099 # Create a dictionary of all local branches to the branches that are dependent
3100 # on it.
3101 tracked_to_dependents = collections.defaultdict(list)
3102 for b in branches.splitlines():
3103 tokens = b.split()
3104 if len(tokens) == 2:
3105 branch_name, tracked = tokens
3106 tracked_to_dependents[tracked].append(branch_name)
3107
vapiera7fbd5a2016-06-16 09:17:49 -07003108 print()
3109 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003110 dependents = []
3111 def traverse_dependents_preorder(branch, padding=''):
3112 dependents_to_process = tracked_to_dependents.get(branch, [])
3113 padding += ' '
3114 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003115 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003116 dependents.append(dependent)
3117 traverse_dependents_preorder(dependent, padding)
3118 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003119 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003120
3121 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003122 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003123 return 0
3124
vapiera7fbd5a2016-06-16 09:17:49 -07003125 print('This command will checkout all dependent branches and run '
3126 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003127 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3128
andybons@chromium.org962f9462016-02-03 20:00:42 +00003129 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003130 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003131 args.extend(['-t', 'Updated patchset dependency'])
3132
rmistry@google.com2dd99862015-06-22 12:22:18 +00003133 # Record all dependents that failed to upload.
3134 failures = {}
3135 # Go through all dependents, checkout the branch and upload.
3136 try:
3137 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003138 print()
3139 print('--------------------------------------')
3140 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003141 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003142 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003143 try:
3144 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003145 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003146 failures[dependent_branch] = 1
3147 except: # pylint: disable=W0702
3148 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003149 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003150 finally:
3151 # Swap back to the original root branch.
3152 RunGit(['checkout', '-q', root_branch])
3153
vapiera7fbd5a2016-06-16 09:17:49 -07003154 print()
3155 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003156 for dependent_branch in dependents:
3157 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003158 print(' %s : %s' % (dependent_branch, upload_status))
3159 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003160
3161 return 0
3162
3163
kmarshall3bff56b2016-06-06 18:31:47 -07003164def CMDarchive(parser, args):
3165 """Archives and deletes branches associated with closed changelists."""
3166 parser.add_option(
3167 '-j', '--maxjobs', action='store', type=int,
3168 help='The maximum number of jobs to use when retrieving review status')
3169 parser.add_option(
3170 '-f', '--force', action='store_true',
3171 help='Bypasses the confirmation prompt.')
3172
3173 auth.add_auth_options(parser)
3174 options, args = parser.parse_args(args)
3175 if args:
3176 parser.error('Unsupported args: %s' % ' '.join(args))
3177 auth_config = auth.extract_auth_config_from_options(options)
3178
3179 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3180 if not branches:
3181 return 0
3182
vapiera7fbd5a2016-06-16 09:17:49 -07003183 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003184 changes = [Changelist(branchref=b, auth_config=auth_config)
3185 for b in branches.splitlines()]
3186 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3187 statuses = get_cl_statuses(changes,
3188 fine_grained=True,
3189 max_processes=options.maxjobs)
3190 proposal = [(cl.GetBranch(),
3191 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3192 for cl, status in statuses
3193 if status == 'closed']
3194 proposal.sort()
3195
3196 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003197 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003198 return 0
3199
3200 current_branch = GetCurrentBranch()
3201
vapiera7fbd5a2016-06-16 09:17:49 -07003202 print('\nBranches with closed issues that will be archived:\n')
3203 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
kmarshall3bff56b2016-06-06 18:31:47 -07003204 for next_item in proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003205 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003206
3207 if any(branch == current_branch for branch, _ in proposal):
3208 print('You are currently on a branch \'%s\' which is associated with a '
3209 'closed codereview issue, so archive cannot proceed. Please '
3210 'checkout another branch and run this command again.' %
3211 current_branch)
3212 return 1
3213
3214 if not options.force:
3215 if ask_for_data('\nProceed with deletion (Y/N)? ').lower() != '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:
3452 issue_arg = ParseIssueNumberArgument(args[0])
3453 if not issue_arg.valid:
3454 parser.print_help()
3455 return 1
3456 target_issue = issue_arg.issue
3457
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003458 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003459
3460 cl = Changelist(
3461 auth_config=auth_config, issue=target_issue,
3462 codereview=options.forced_codereview)
3463
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003464 if not cl.GetIssue():
3465 DieWithError('This branch has no associated changelist.')
3466 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003467
smut@google.com34fb6b12015-07-13 20:03:26 +00003468 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003469 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003470 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003471
3472 if options.new_description:
3473 text = options.new_description
3474 if text == '-':
3475 text = '\n'.join(l.rstrip() for l in sys.stdin)
3476
3477 description.set_description(text)
3478 else:
3479 description.prompt()
3480
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003481 if cl.GetDescription() != description.description:
3482 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003483 return 0
3484
3485
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003486def CreateDescriptionFromLog(args):
3487 """Pulls out the commit log to use as a base for the CL description."""
3488 log_args = []
3489 if len(args) == 1 and not args[0].endswith('.'):
3490 log_args = [args[0] + '..']
3491 elif len(args) == 1 and args[0].endswith('...'):
3492 log_args = [args[0][:-1]]
3493 elif len(args) == 2:
3494 log_args = [args[0] + '..' + args[1]]
3495 else:
3496 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003497 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003498
3499
thestig@chromium.org44202a22014-03-11 19:22:18 +00003500def CMDlint(parser, args):
3501 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003502 parser.add_option('--filter', action='append', metavar='-x,+y',
3503 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003504 auth.add_auth_options(parser)
3505 options, args = parser.parse_args(args)
3506 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003507
3508 # Access to a protected member _XX of a client class
3509 # pylint: disable=W0212
3510 try:
3511 import cpplint
3512 import cpplint_chromium
3513 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003514 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003515 return 1
3516
3517 # Change the current working directory before calling lint so that it
3518 # shows the correct base.
3519 previous_cwd = os.getcwd()
3520 os.chdir(settings.GetRoot())
3521 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003522 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003523 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3524 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003525 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003526 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003527 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003528
3529 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003530 command = args + files
3531 if options.filter:
3532 command = ['--filter=' + ','.join(options.filter)] + command
3533 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003534
3535 white_regex = re.compile(settings.GetLintRegex())
3536 black_regex = re.compile(settings.GetLintIgnoreRegex())
3537 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3538 for filename in filenames:
3539 if white_regex.match(filename):
3540 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07003541 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003542 else:
3543 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3544 extra_check_functions)
3545 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003546 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003547 finally:
3548 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07003549 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003550 if cpplint._cpplint_state.error_count != 0:
3551 return 1
3552 return 0
3553
3554
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003555def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003556 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003557 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003558 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003559 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003560 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003561 auth.add_auth_options(parser)
3562 options, args = parser.parse_args(args)
3563 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003564
sbc@chromium.org71437c02015-04-09 19:29:40 +00003565 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07003566 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003567 return 1
3568
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003569 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003570 if args:
3571 base_branch = args[0]
3572 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003573 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003574 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003575
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003576 cl.RunHook(
3577 committing=not options.upload,
3578 may_prompt=False,
3579 verbose=options.verbose,
3580 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003581 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003582
3583
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003584def GenerateGerritChangeId(message):
3585 """Returns Ixxxxxx...xxx change id.
3586
3587 Works the same way as
3588 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3589 but can be called on demand on all platforms.
3590
3591 The basic idea is to generate git hash of a state of the tree, original commit
3592 message, author/committer info and timestamps.
3593 """
3594 lines = []
3595 tree_hash = RunGitSilent(['write-tree'])
3596 lines.append('tree %s' % tree_hash.strip())
3597 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3598 if code == 0:
3599 lines.append('parent %s' % parent.strip())
3600 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3601 lines.append('author %s' % author.strip())
3602 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3603 lines.append('committer %s' % committer.strip())
3604 lines.append('')
3605 # Note: Gerrit's commit-hook actually cleans message of some lines and
3606 # whitespace. This code is not doing this, but it clearly won't decrease
3607 # entropy.
3608 lines.append(message)
3609 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3610 stdin='\n'.join(lines))
3611 return 'I%s' % change_hash.strip()
3612
3613
wittman@chromium.org455dc922015-01-26 20:15:50 +00003614def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3615 """Computes the remote branch ref to use for the CL.
3616
3617 Args:
3618 remote (str): The git remote for the CL.
3619 remote_branch (str): The git remote branch for the CL.
3620 target_branch (str): The target branch specified by the user.
3621 pending_prefix (str): The pending prefix from the settings.
3622 """
3623 if not (remote and remote_branch):
3624 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003625
wittman@chromium.org455dc922015-01-26 20:15:50 +00003626 if target_branch:
3627 # Cannonicalize branch references to the equivalent local full symbolic
3628 # refs, which are then translated into the remote full symbolic refs
3629 # below.
3630 if '/' not in target_branch:
3631 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3632 else:
3633 prefix_replacements = (
3634 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3635 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3636 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3637 )
3638 match = None
3639 for regex, replacement in prefix_replacements:
3640 match = re.search(regex, target_branch)
3641 if match:
3642 remote_branch = target_branch.replace(match.group(0), replacement)
3643 break
3644 if not match:
3645 # This is a branch path but not one we recognize; use as-is.
3646 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003647 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3648 # Handle the refs that need to land in different refs.
3649 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003650
wittman@chromium.org455dc922015-01-26 20:15:50 +00003651 # Create the true path to the remote branch.
3652 # Does the following translation:
3653 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3654 # * refs/remotes/origin/master -> refs/heads/master
3655 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3656 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3657 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3658 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3659 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3660 'refs/heads/')
3661 elif remote_branch.startswith('refs/remotes/branch-heads'):
3662 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3663 # If a pending prefix exists then replace refs/ with it.
3664 if pending_prefix:
3665 remote_branch = remote_branch.replace('refs/', pending_prefix)
3666 return remote_branch
3667
3668
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003669def cleanup_list(l):
3670 """Fixes a list so that comma separated items are put as individual items.
3671
3672 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3673 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3674 """
3675 items = sum((i.split(',') for i in l), [])
3676 stripped_items = (i.strip() for i in items)
3677 return sorted(filter(None, stripped_items))
3678
3679
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003680@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003681def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003682 """Uploads the current changelist to codereview.
3683
3684 Can skip dependency patchset uploads for a branch by running:
3685 git config branch.branch_name.skip-deps-uploads True
3686 To unset run:
3687 git config --unset branch.branch_name.skip-deps-uploads
3688 Can also set the above globally by using the --global flag.
3689 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003690 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3691 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003692 parser.add_option('--bypass-watchlists', action='store_true',
3693 dest='bypass_watchlists',
3694 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003695 parser.add_option('-f', action='store_true', dest='force',
3696 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003697 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003698 parser.add_option('-t', dest='title',
3699 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003700 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003701 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003702 help='reviewer email addresses')
3703 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003704 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003705 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003706 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003707 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003708 parser.add_option('--emulate_svn_auto_props',
3709 '--emulate-svn-auto-props',
3710 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003711 dest="emulate_svn_auto_props",
3712 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003713 parser.add_option('-c', '--use-commit-queue', action='store_true',
3714 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003715 parser.add_option('--private', action='store_true',
3716 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003717 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003718 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003719 metavar='TARGET',
3720 help='Apply CL to remote ref TARGET. ' +
3721 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003722 parser.add_option('--squash', action='store_true',
3723 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003724 parser.add_option('--no-squash', action='store_true',
3725 help='Don\'t squash multiple commits into one ' +
3726 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003727 parser.add_option('--email', default=None,
3728 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003729 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3730 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003731 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3732 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003733 help='Send the patchset to do a CQ dry run right after '
3734 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003735 parser.add_option('--dependencies', action='store_true',
3736 help='Uploads CLs of all the local branches that depend on '
3737 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003738
rmistry@google.com2dd99862015-06-22 12:22:18 +00003739 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003740 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003741 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003742 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003743 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003744 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003745 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003746
sbc@chromium.org71437c02015-04-09 19:29:40 +00003747 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003748 return 1
3749
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003750 options.reviewers = cleanup_list(options.reviewers)
3751 options.cc = cleanup_list(options.cc)
3752
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003753 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3754 settings.GetIsGerrit()
3755
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003756 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00003757 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003758
3759
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003760def IsSubmoduleMergeCommit(ref):
3761 # When submodules are added to the repo, we expect there to be a single
3762 # non-git-svn merge commit at remote HEAD with a signature comment.
3763 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003764 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003765 return RunGit(cmd) != ''
3766
3767
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003768def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003769 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003770
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003771 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3772 upstream and closes the issue automatically and atomically.
3773
3774 Otherwise (in case of Rietveld):
3775 Squashes branch into a single commit.
3776 Updates changelog with metadata (e.g. pointer to review).
3777 Pushes/dcommits the code upstream.
3778 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003779 """
3780 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3781 help='bypass upload presubmit hook')
3782 parser.add_option('-m', dest='message',
3783 help="override review description")
3784 parser.add_option('-f', action='store_true', dest='force',
3785 help="force yes to questions (don't prompt)")
3786 parser.add_option('-c', dest='contributor',
3787 help="external contributor for patch (appended to " +
3788 "description and used as author for git). Should be " +
3789 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003790 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003791 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003792 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003793 auth_config = auth.extract_auth_config_from_options(options)
3794
3795 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003796
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003797 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3798 if cl.IsGerrit():
3799 if options.message:
3800 # This could be implemented, but it requires sending a new patch to
3801 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3802 # Besides, Gerrit has the ability to change the commit message on submit
3803 # automatically, thus there is no need to support this option (so far?).
3804 parser.error('-m MESSAGE option is not supported for Gerrit.')
3805 if options.contributor:
3806 parser.error(
3807 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3808 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3809 'the contributor\'s "name <email>". If you can\'t upload such a '
3810 'commit for review, contact your repository admin and request'
3811 '"Forge-Author" permission.')
3812 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3813 options.verbose)
3814
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003815 current = cl.GetBranch()
3816 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3817 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07003818 print()
3819 print('Attempting to push branch %r into another local branch!' % current)
3820 print()
3821 print('Either reparent this branch on top of origin/master:')
3822 print(' git reparent-branch --root')
3823 print()
3824 print('OR run `git rebase-update` if you think the parent branch is ')
3825 print('already committed.')
3826 print()
3827 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003828 return 1
3829
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003830 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003831 # Default to merging against our best guess of the upstream branch.
3832 args = [cl.GetUpstreamBranch()]
3833
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003834 if options.contributor:
3835 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07003836 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003837 return 1
3838
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003839 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003840 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003841
sbc@chromium.org71437c02015-04-09 19:29:40 +00003842 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003843 return 1
3844
3845 # This rev-list syntax means "show all commits not in my branch that
3846 # are in base_branch".
3847 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3848 base_branch]).splitlines()
3849 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07003850 print('Base branch "%s" has %d commits '
3851 'not in this branch.' % (base_branch, len(upstream_commits)))
3852 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003853 return 1
3854
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003855 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003856 svn_head = None
3857 if cmd == 'dcommit' or base_has_submodules:
3858 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
3859 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003860
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003861 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003862 # If the base_head is a submodule merge commit, the first parent of the
3863 # base_head should be a git-svn commit, which is what we're interested in.
3864 base_svn_head = base_branch
3865 if base_has_submodules:
3866 base_svn_head += '^1'
3867
3868 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003869 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07003870 print('This branch has %d additional commits not upstreamed yet.'
3871 % len(extra_commits.splitlines()))
3872 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
3873 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003874 return 1
3875
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003876 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003877 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003878 author = None
3879 if options.contributor:
3880 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003881 hook_results = cl.RunHook(
3882 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003883 may_prompt=not options.force,
3884 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003885 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003886 if not hook_results.should_continue():
3887 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003888
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003889 # Check the tree status if the tree status URL is set.
3890 status = GetTreeStatus()
3891 if 'closed' == status:
3892 print('The tree is closed. Please wait for it to reopen. Use '
3893 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3894 return 1
3895 elif 'unknown' == status:
3896 print('Unable to determine tree status. Please verify manually and '
3897 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3898 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003899
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003900 change_desc = ChangeDescription(options.message)
3901 if not change_desc.description and cl.GetIssue():
3902 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003903
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003904 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00003905 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003906 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00003907 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003908 print('No description set.')
3909 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00003910 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003911
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003912 # Keep a separate copy for the commit message, because the commit message
3913 # contains the link to the Rietveld issue, while the Rietveld message contains
3914 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003915 # Keep a separate copy for the commit message.
3916 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00003917 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003918
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003919 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00003920 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00003921 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00003922 # after it. Add a period on a new line to circumvent this. Also add a space
3923 # before the period to make sure that Gitiles continues to correctly resolve
3924 # the URL.
3925 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003926 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003927 commit_desc.append_footer('Patch from %s.' % options.contributor)
3928
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00003929 print('Description:')
3930 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003931
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003932 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003933 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00003934 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003935
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003936 # We want to squash all this branch's commits into one commit with the proper
3937 # description. We do this by doing a "reset --soft" to the base branch (which
3938 # keeps the working copy the same), then dcommitting that. If origin/master
3939 # has a submodule merge commit, we'll also need to cherry-pick the squashed
3940 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003941 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003942 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
3943 # Delete the branches if they exist.
3944 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
3945 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
3946 result = RunGitWithCode(showref_cmd)
3947 if result[0] == 0:
3948 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003949
3950 # We might be in a directory that's present in this branch but not in the
3951 # trunk. Move up to the top of the tree so that git commands that expect a
3952 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003953 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003954 if rel_base_path:
3955 os.chdir(rel_base_path)
3956
3957 # Stuff our change into the merge branch.
3958 # We wrap in a try...finally block so if anything goes wrong,
3959 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003960 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003961 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003962 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003963 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003964 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00003965 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003966 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003967 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003968 RunGit(
3969 [
3970 'commit', '--author', options.contributor,
3971 '-m', commit_desc.description,
3972 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003973 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003974 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003975 if base_has_submodules:
3976 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
3977 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
3978 RunGit(['checkout', CHERRY_PICK_BRANCH])
3979 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003980 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003981 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003982 mirror = settings.GetGitMirror(remote)
3983 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003984 pending_prefix = settings.GetPendingRefPrefix()
3985 if not pending_prefix or branch.startswith(pending_prefix):
3986 # If not using refs/pending/heads/* at all, or target ref is already set
3987 # to pending, then push to the target ref directly.
3988 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003989 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003990 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003991 else:
3992 # Cherry-pick the change on top of pending ref and then push it.
3993 assert branch.startswith('refs/'), branch
3994 assert pending_prefix[-1] == '/', pending_prefix
3995 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003996 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003997 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003998 if retcode == 0:
3999 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004000 else:
4001 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004002 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00004003 'svn', 'dcommit',
4004 '-C%s' % options.similarity,
4005 '--no-rebase', '--rmdir',
4006 ]
4007 if settings.GetForceHttpsCommitUrl():
4008 # Allow forcing https commit URLs for some projects that don't allow
4009 # committing to http URLs (like Google Code).
4010 remote_url = cl.GetGitSvnRemoteUrl()
4011 if urlparse.urlparse(remote_url).scheme == 'http':
4012 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004013 cmd_args.append('--commit-url=%s' % remote_url)
4014 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004015 if 'Committed r' in output:
4016 revision = re.match(
4017 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4018 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004019 finally:
4020 # And then swap back to the original branch and clean up.
4021 RunGit(['checkout', '-q', cl.GetBranch()])
4022 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004023 if base_has_submodules:
4024 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004025
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004026 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004027 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004028 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004029
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004030 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004031 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004032 try:
4033 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4034 # We set pushed_to_pending to False, since it made it all the way to the
4035 # real ref.
4036 pushed_to_pending = False
4037 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004038 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004039
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004040 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004041 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004042 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004043 if not to_pending:
4044 if viewvc_url and revision:
4045 change_desc.append_footer(
4046 'Committed: %s%s' % (viewvc_url, revision))
4047 elif revision:
4048 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004049 print('Closing issue '
4050 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004051 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004052 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004053 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004054 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004055 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004056 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004057 if options.bypass_hooks:
4058 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4059 else:
4060 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004061 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004062 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004063
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004064 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004065 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004066 print('The commit is in the pending queue (%s).' % pending_ref)
4067 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4068 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004069
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004070 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4071 if os.path.isfile(hook):
4072 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004073
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004074 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004075
4076
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004077def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004078 print()
4079 print('Waiting for commit to be landed on %s...' % real_ref)
4080 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004081 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4082 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004083 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004084
4085 loop = 0
4086 while True:
4087 sys.stdout.write('fetching (%d)... \r' % loop)
4088 sys.stdout.flush()
4089 loop += 1
4090
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004091 if mirror:
4092 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004093 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4094 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4095 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4096 for commit in commits.splitlines():
4097 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004098 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004099 return commit
4100
4101 current_rev = to_rev
4102
4103
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004104def PushToGitPending(remote, pending_ref, upstream_ref):
4105 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4106
4107 Returns:
4108 (retcode of last operation, output log of last operation).
4109 """
4110 assert pending_ref.startswith('refs/'), pending_ref
4111 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4112 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4113 code = 0
4114 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004115 max_attempts = 3
4116 attempts_left = max_attempts
4117 while attempts_left:
4118 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004119 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004120 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004121
4122 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004123 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004124 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004125 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004126 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004127 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004128 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004129 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004130 continue
4131
4132 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004133 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004134 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004135 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004136 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004137 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4138 'the following files have merge conflicts:' % pending_ref)
4139 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4140 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004141 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004142 return code, out
4143
4144 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004145 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004146 code, out = RunGitWithCode(
4147 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4148 if code == 0:
4149 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004150 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004151 return code, out
4152
vapiera7fbd5a2016-06-16 09:17:49 -07004153 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004154 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004155 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004156 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004157 print('Fatal push error. Make sure your .netrc credentials and git '
4158 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004159 return code, out
4160
vapiera7fbd5a2016-06-16 09:17:49 -07004161 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004162 return code, out
4163
4164
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004165def IsFatalPushFailure(push_stdout):
4166 """True if retrying push won't help."""
4167 return '(prohibited by Gerrit)' in push_stdout
4168
4169
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004170@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004171def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004172 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004173 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004174 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004175 # If it looks like previous commits were mirrored with git-svn.
4176 message = """This repository appears to be a git-svn mirror, but no
4177upstream SVN master is set. You probably need to run 'git auto-svn' once."""
4178 else:
4179 message = """This doesn't appear to be an SVN repository.
4180If your project has a true, writeable git repository, you probably want to run
4181'git cl land' instead.
4182If your project has a git mirror of an upstream SVN master, you probably need
4183to run 'git svn init'.
4184
4185Using the wrong command might cause your commit to appear to succeed, and the
4186review to be closed, without actually landing upstream. If you choose to
4187proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004188 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004189 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
tandrii3bb82ff2016-06-17 07:36:36 -07004190 # TODO(tandrii): kill this post SVN migration with
4191 # https://codereview.chromium.org/2076683002
4192 print('WARNING: chrome infrastructure is migrating SVN repos to Git.\n'
4193 'Please let us know of this project you are committing to:'
4194 ' http://crbug.com/600451')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004195 return SendUpstream(parser, args, 'dcommit')
4196
4197
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004198@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004199def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004200 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004201 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004202 print('This appears to be an SVN repository.')
4203 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004204 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004205 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004206 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004207
4208
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004209@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004210def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004211 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004212 parser.add_option('-b', dest='newbranch',
4213 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004214 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004215 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004216 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4217 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004218 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004219 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004220 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004221 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004222 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004223 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004224
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004225
4226 group = optparse.OptionGroup(
4227 parser,
4228 'Options for continuing work on the current issue uploaded from a '
4229 'different clone (e.g. different machine). Must be used independently '
4230 'from the other options. No issue number should be specified, and the '
4231 'branch must have an issue number associated with it')
4232 group.add_option('--reapply', action='store_true', dest='reapply',
4233 help='Reset the branch and reapply the issue.\n'
4234 'CAUTION: This will undo any local changes in this '
4235 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004236
4237 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004238 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004239 parser.add_option_group(group)
4240
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004241 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004242 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004243 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004244 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004245 auth_config = auth.extract_auth_config_from_options(options)
4246
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004247
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004248 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004249 if options.newbranch:
4250 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004251 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004252 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004253
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004254 cl = Changelist(auth_config=auth_config,
4255 codereview=options.forced_codereview)
4256 if not cl.GetIssue():
4257 parser.error('current branch must have an associated issue')
4258
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004259 upstream = cl.GetUpstreamBranch()
4260 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004261 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004262
4263 RunGit(['reset', '--hard', upstream])
4264 if options.pull:
4265 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004266
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004267 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4268 options.directory)
4269
4270 if len(args) != 1 or not args[0]:
4271 parser.error('Must specify issue number or url')
4272
4273 # We don't want uncommitted changes mixed up with the patch.
4274 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004275 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004276
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004277 if options.newbranch:
4278 if options.force:
4279 RunGit(['branch', '-D', options.newbranch],
4280 stderr=subprocess2.PIPE, error_ok=True)
4281 RunGit(['new-branch', options.newbranch])
4282
4283 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4284
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004285 if cl.IsGerrit():
4286 if options.reject:
4287 parser.error('--reject is not supported with Gerrit codereview.')
4288 if options.nocommit:
4289 parser.error('--nocommit is not supported with Gerrit codereview.')
4290 if options.directory:
4291 parser.error('--directory is not supported with Gerrit codereview.')
4292
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004293 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004294 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004295
4296
4297def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004298 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004299 # Provide a wrapper for git svn rebase to help avoid accidental
4300 # git svn dcommit.
4301 # It's the only command that doesn't use parser at all since we just defer
4302 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004303
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004304 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004305
4306
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004307def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004308 """Fetches the tree status and returns either 'open', 'closed',
4309 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004310 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004311 if url:
4312 status = urllib2.urlopen(url).read().lower()
4313 if status.find('closed') != -1 or status == '0':
4314 return 'closed'
4315 elif status.find('open') != -1 or status == '1':
4316 return 'open'
4317 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004318 return 'unset'
4319
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004320
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004321def GetTreeStatusReason():
4322 """Fetches the tree status from a json url and returns the message
4323 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004324 url = settings.GetTreeStatusUrl()
4325 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004326 connection = urllib2.urlopen(json_url)
4327 status = json.loads(connection.read())
4328 connection.close()
4329 return status['message']
4330
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004331
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004332def GetBuilderMaster(bot_list):
4333 """For a given builder, fetch the master from AE if available."""
4334 map_url = 'https://builders-map.appspot.com/'
4335 try:
4336 master_map = json.load(urllib2.urlopen(map_url))
4337 except urllib2.URLError as e:
4338 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4339 (map_url, e))
4340 except ValueError as e:
4341 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4342 if not master_map:
4343 return None, 'Failed to build master map.'
4344
4345 result_master = ''
4346 for bot in bot_list:
4347 builder = bot.split(':', 1)[0]
4348 master_list = master_map.get(builder, [])
4349 if not master_list:
4350 return None, ('No matching master for builder %s.' % builder)
4351 elif len(master_list) > 1:
4352 return None, ('The builder name %s exists in multiple masters %s.' %
4353 (builder, master_list))
4354 else:
4355 cur_master = master_list[0]
4356 if not result_master:
4357 result_master = cur_master
4358 elif result_master != cur_master:
4359 return None, 'The builders do not belong to the same master.'
4360 return result_master, None
4361
4362
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004363def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004364 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004365 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004366 status = GetTreeStatus()
4367 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004368 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004369 return 2
4370
vapiera7fbd5a2016-06-16 09:17:49 -07004371 print('The tree is %s' % status)
4372 print()
4373 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004374 if status != 'open':
4375 return 1
4376 return 0
4377
4378
maruel@chromium.org15192402012-09-06 12:38:29 +00004379def CMDtry(parser, args):
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004380 """Triggers try jobs through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00004381 group = optparse.OptionGroup(parser, "Try job options")
4382 group.add_option(
4383 "-b", "--bot", action="append",
4384 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
4385 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004386 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00004387 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004388 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00004389 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004390 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00004391 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004392 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004393 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004394 "-r", "--revision",
4395 help="Revision to use for the try job; default: the "
4396 "revision will be determined by the try server; see "
4397 "its waterfall for more info")
4398 group.add_option(
4399 "-c", "--clobber", action="store_true", default=False,
4400 help="Force a clobber before building; e.g. don't do an "
4401 "incremental build")
4402 group.add_option(
4403 "--project",
4404 help="Override which project to use. Projects are defined "
4405 "server-side to define what default bot set to use")
4406 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004407 "-p", "--property", dest="properties", action="append", default=[],
4408 help="Specify generic properties in the form -p key1=value1 -p "
4409 "key2=value2 etc (buildbucket only). The value will be treated as "
4410 "json if decodable, or as string otherwise.")
4411 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004412 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004413 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004414 "--use-rietveld", action="store_true", default=False,
4415 help="Use Rietveld to trigger try jobs.")
4416 group.add_option(
4417 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4418 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004419 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004420 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004421 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004422 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004423
machenbach@chromium.org45453142015-09-15 08:45:22 +00004424 if options.use_rietveld and options.properties:
4425 parser.error('Properties can only be specified with buildbucket')
4426
4427 # Make sure that all properties are prop=value pairs.
4428 bad_params = [x for x in options.properties if '=' not in x]
4429 if bad_params:
4430 parser.error('Got properties with missing "=": %s' % bad_params)
4431
maruel@chromium.org15192402012-09-06 12:38:29 +00004432 if args:
4433 parser.error('Unknown arguments: %s' % args)
4434
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004435 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004436 if not cl.GetIssue():
4437 parser.error('Need to upload first')
4438
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004439 if cl.IsGerrit():
4440 parser.error(
4441 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4442 'If your project has Commit Queue, dry run is a workaround:\n'
4443 ' git cl set-commit --dry-run')
4444 # Code below assumes Rietveld issue.
4445 # TODO(tandrii): actually implement for Gerrit http://crbug.com/599931.
4446
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004447 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004448 if props.get('closed'):
4449 parser.error('Cannot send tryjobs for a closed CL')
4450
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004451 if props.get('private'):
4452 parser.error('Cannot use trybots with private issue')
4453
maruel@chromium.org15192402012-09-06 12:38:29 +00004454 if not options.name:
4455 options.name = cl.GetBranch()
4456
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004457 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004458 options.master, err_msg = GetBuilderMaster(options.bot)
4459 if err_msg:
4460 parser.error('Tryserver master cannot be found because: %s\n'
4461 'Please manually specify the tryserver master'
4462 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004463
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004464 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004465 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004466 if not options.bot:
4467 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004468
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004469 # Get try masters from PRESUBMIT.py files.
4470 masters = presubmit_support.DoGetTryMasters(
4471 change,
4472 change.LocalPaths(),
4473 settings.GetRoot(),
4474 None,
4475 None,
4476 options.verbose,
4477 sys.stdout)
4478 if masters:
4479 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004480
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004481 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4482 options.bot = presubmit_support.DoGetTrySlaves(
4483 change,
4484 change.LocalPaths(),
4485 settings.GetRoot(),
4486 None,
4487 None,
4488 options.verbose,
4489 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004490
4491 if not options.bot:
4492 # Get try masters from cq.cfg if any.
4493 # TODO(tandrii): some (but very few) projects store cq.cfg in different
4494 # location.
4495 cq_cfg = os.path.join(change.RepositoryRoot(),
4496 'infra', 'config', 'cq.cfg')
4497 if os.path.exists(cq_cfg):
4498 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00004499 cq_masters = commit_queue.get_master_builder_map(
4500 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004501 for master, builders in cq_masters.iteritems():
4502 for builder in builders:
4503 # Skip presubmit builders, because these will fail without LGTM.
machenbach@chromium.org2403e802016-04-29 12:34:42 +00004504 masters.setdefault(master, {})[builder] = ['defaulttests']
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004505 if masters:
tandriib93dd2b2016-06-07 08:03:08 -07004506 print('Loaded default bots from CQ config (%s)' % cq_cfg)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004507 return masters
tandriib93dd2b2016-06-07 08:03:08 -07004508 else:
4509 print('CQ config exists (%s) but has no try bots listed' % cq_cfg)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004510
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004511 if not options.bot:
4512 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00004513
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004514 builders_and_tests = {}
4515 # TODO(machenbach): The old style command-line options don't support
4516 # multiple try masters yet.
4517 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4518 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4519
4520 for bot in old_style:
4521 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004522 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004523 elif ',' in bot:
4524 parser.error('Specify one bot per --bot flag')
4525 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004526 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004527
4528 for bot, tests in new_style:
4529 builders_and_tests.setdefault(bot, []).extend(tests)
4530
4531 # Return a master map with one master to be backwards compatible. The
4532 # master name defaults to an empty string, which will cause the master
4533 # not to be set on rietveld (deprecated).
4534 return {options.master: builders_and_tests}
4535
4536 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004537
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004538 for builders in masters.itervalues():
4539 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004540 print('ERROR You are trying to send a job to a triggered bot. This type '
4541 'of bot requires an\ninitial job from a parent (usually a builder).'
4542 ' Instead send your job to the parent.\n'
4543 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004544 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004545
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004546 patchset = cl.GetMostRecentPatchset()
4547 if patchset and patchset != cl.GetPatchset():
4548 print(
4549 '\nWARNING Mismatch between local config and server. Did a previous '
4550 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4551 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004552 if options.luci:
4553 trigger_luci_job(cl, masters, options)
4554 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004555 try:
4556 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4557 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004558 print('ERROR: %s' % ex)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004559 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004560 except Exception as e:
4561 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
vapiera7fbd5a2016-06-16 09:17:49 -07004562 print('ERROR: Exception when trying to trigger tryjobs: %s\n%s' %
4563 (e, stacktrace))
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004564 return 1
4565 else:
4566 try:
4567 cl.RpcServer().trigger_distributed_try_jobs(
4568 cl.GetIssue(), patchset, options.name, options.clobber,
4569 options.revision, masters)
4570 except urllib2.HTTPError as e:
4571 if e.code == 404:
4572 print('404 from rietveld; '
4573 'did you mean to use "git try" instead of "git cl try"?')
4574 return 1
4575 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004576
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004577 for (master, builders) in sorted(masters.iteritems()):
4578 if master:
vapiera7fbd5a2016-06-16 09:17:49 -07004579 print('Master: %s' % master)
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004580 length = max(len(builder) for builder in builders)
4581 for builder in sorted(builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004582 print(' %*s: %s' % (length, builder, ','.join(builders[builder])))
maruel@chromium.org15192402012-09-06 12:38:29 +00004583 return 0
4584
4585
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004586def CMDtry_results(parser, args):
4587 group = optparse.OptionGroup(parser, "Try job results options")
4588 group.add_option(
4589 "-p", "--patchset", type=int, help="patchset number if not current.")
4590 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004591 "--print-master", action='store_true', help="print master name as well.")
4592 group.add_option(
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004593 "--color", action='store_true', default=setup_color.IS_TTY,
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004594 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004595 group.add_option(
4596 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4597 help="Host of buildbucket. The default host is %default.")
4598 parser.add_option_group(group)
4599 auth.add_auth_options(parser)
4600 options, args = parser.parse_args(args)
4601 if args:
4602 parser.error('Unrecognized args: %s' % ' '.join(args))
4603
4604 auth_config = auth.extract_auth_config_from_options(options)
4605 cl = Changelist(auth_config=auth_config)
4606 if not cl.GetIssue():
4607 parser.error('Need to upload first')
4608
4609 if not options.patchset:
4610 options.patchset = cl.GetMostRecentPatchset()
4611 if options.patchset and options.patchset != cl.GetPatchset():
4612 print(
4613 '\nWARNING Mismatch between local config and server. Did a previous '
4614 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4615 'Continuing using\npatchset %s.\n' % options.patchset)
4616 try:
4617 jobs = fetch_try_jobs(auth_config, cl, options)
4618 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004619 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004620 return 1
4621 except Exception as e:
4622 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
vapiera7fbd5a2016-06-16 09:17:49 -07004623 print('ERROR: Exception when trying to fetch tryjobs: %s\n%s' %
4624 (e, stacktrace))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004625 return 1
4626 print_tryjobs(options, jobs)
4627 return 0
4628
4629
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004630@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004631def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004632 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004633 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004634 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004635 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004636
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004637 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004638 if args:
4639 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004640 branch = cl.GetBranch()
4641 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004642 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004643 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004644
4645 # Clear configured merge-base, if there is one.
4646 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004647 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004648 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004649 return 0
4650
4651
thestig@chromium.org00858c82013-12-02 23:08:03 +00004652def CMDweb(parser, args):
4653 """Opens the current CL in the web browser."""
4654 _, args = parser.parse_args(args)
4655 if args:
4656 parser.error('Unrecognized args: %s' % ' '.join(args))
4657
4658 issue_url = Changelist().GetIssueURL()
4659 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004660 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004661 return 1
4662
4663 webbrowser.open(issue_url)
4664 return 0
4665
4666
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004667def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004668 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004669 parser.add_option('-d', '--dry-run', action='store_true',
4670 help='trigger in dry run mode')
4671 parser.add_option('-c', '--clear', action='store_true',
4672 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004673 auth.add_auth_options(parser)
4674 options, args = parser.parse_args(args)
4675 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004676 if args:
4677 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004678 if options.dry_run and options.clear:
4679 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4680
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004681 cl = Changelist(auth_config=auth_config)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004682 if options.clear:
4683 state = _CQState.CLEAR
4684 elif options.dry_run:
4685 state = _CQState.DRY_RUN
4686 else:
4687 state = _CQState.COMMIT
4688 if not cl.GetIssue():
4689 parser.error('Must upload the issue first')
4690 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004691 return 0
4692
4693
groby@chromium.org411034a2013-02-26 15:12:01 +00004694def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004695 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004696 auth.add_auth_options(parser)
4697 options, args = parser.parse_args(args)
4698 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004699 if args:
4700 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004701 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00004702 # Ensure there actually is an issue to close.
4703 cl.GetDescription()
4704 cl.CloseIssue()
4705 return 0
4706
4707
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004708def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004709 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004710 auth.add_auth_options(parser)
4711 options, args = parser.parse_args(args)
4712 auth_config = auth.extract_auth_config_from_options(options)
4713 if args:
4714 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004715
4716 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004717 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004718 # Staged changes would be committed along with the patch from last
4719 # upload, hence counted toward the "last upload" side in the final
4720 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004721 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004722 return 1
4723
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004724 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004725 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004726 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004727 if not issue:
4728 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004729 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004730 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004731
4732 # Create a new branch based on the merge-base
4733 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004734 # Clear cached branch in cl object, to avoid overwriting original CL branch
4735 # properties.
4736 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004737 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004738 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004739 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004740 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004741 return rtn
4742
wychen@chromium.org06928532015-02-03 02:11:29 +00004743 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004744 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00004745 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004746 finally:
4747 RunGit(['checkout', '-q', branch])
4748 RunGit(['branch', '-D', TMP_BRANCH])
4749
4750 return 0
4751
4752
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004753def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004754 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004755 parser.add_option(
4756 '--no-color',
4757 action='store_true',
4758 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004759 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004760 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004761 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004762
4763 author = RunGit(['config', 'user.email']).strip() or None
4764
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004765 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004766
4767 if args:
4768 if len(args) > 1:
4769 parser.error('Unknown args')
4770 base_branch = args[0]
4771 else:
4772 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004773 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004774
4775 change = cl.GetChange(base_branch, None)
4776 return owners_finder.OwnersFinder(
4777 [f.LocalPath() for f in
4778 cl.GetChange(base_branch, None).AffectedFiles()],
4779 change.RepositoryRoot(), author,
4780 fopen=file, os_path=os.path, glob=glob.glob,
4781 disable_color=options.no_color).run()
4782
4783
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004784def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004785 """Generates a diff command."""
4786 # Generate diff for the current branch's changes.
4787 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4788 upstream_commit, '--' ]
4789
4790 if args:
4791 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004792 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004793 diff_cmd.append(arg)
4794 else:
4795 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004796
4797 return diff_cmd
4798
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004799def MatchingFileType(file_name, extensions):
4800 """Returns true if the file name ends with one of the given extensions."""
4801 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004802
enne@chromium.org555cfe42014-01-29 18:21:39 +00004803@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004804def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004805 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00004806 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07004807 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004808 parser.add_option('--full', action='store_true',
4809 help='Reformat the full content of all touched files')
4810 parser.add_option('--dry-run', action='store_true',
4811 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004812 parser.add_option('--python', action='store_true',
4813 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004814 parser.add_option('--diff', action='store_true',
4815 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004816 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004817
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004818 # git diff generates paths against the root of the repository. Change
4819 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004820 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004821 if rel_base_path:
4822 os.chdir(rel_base_path)
4823
digit@chromium.org29e47272013-05-17 17:01:46 +00004824 # Grab the merge-base commit, i.e. the upstream commit of the current
4825 # branch when it was created or the last time it was rebased. This is
4826 # to cover the case where the user may have called "git fetch origin",
4827 # moving the origin branch to a newer commit, but hasn't rebased yet.
4828 upstream_commit = None
4829 cl = Changelist()
4830 upstream_branch = cl.GetUpstreamBranch()
4831 if upstream_branch:
4832 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4833 upstream_commit = upstream_commit.strip()
4834
4835 if not upstream_commit:
4836 DieWithError('Could not find base commit for this branch. '
4837 'Are you in detached state?')
4838
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004839 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4840 diff_output = RunGit(changed_files_cmd)
4841 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004842 # Filter out files deleted by this CL
4843 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004844
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004845 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4846 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4847 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004848 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004849
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004850 top_dir = os.path.normpath(
4851 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
4852
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004853 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4854 # formatted. This is used to block during the presubmit.
4855 return_value = 0
4856
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004857 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004858 # Locate the clang-format binary in the checkout
4859 try:
4860 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07004861 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004862 DieWithError(e)
4863
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004864 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004865 cmd = [clang_format_tool]
4866 if not opts.dry_run and not opts.diff:
4867 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004868 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004869 if opts.diff:
4870 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004871 else:
4872 env = os.environ.copy()
4873 env['PATH'] = str(os.path.dirname(clang_format_tool))
4874 try:
4875 script = clang_format.FindClangFormatScriptInChromiumTree(
4876 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07004877 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004878 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004879
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004880 cmd = [sys.executable, script, '-p0']
4881 if not opts.dry_run and not opts.diff:
4882 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004883
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004884 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
4885 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004886
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004887 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
4888 if opts.diff:
4889 sys.stdout.write(stdout)
4890 if opts.dry_run and len(stdout) > 0:
4891 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004892
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004893 # Similar code to above, but using yapf on .py files rather than clang-format
4894 # on C/C++ files
4895 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004896 yapf_tool = gclient_utils.FindExecutable('yapf')
4897 if yapf_tool is None:
4898 DieWithError('yapf not found in PATH')
4899
4900 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004901 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004902 cmd = [yapf_tool]
4903 if not opts.dry_run and not opts.diff:
4904 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004905 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004906 if opts.diff:
4907 sys.stdout.write(stdout)
4908 else:
4909 # TODO(sbc): yapf --lines mode still has some issues.
4910 # https://github.com/google/yapf/issues/154
4911 DieWithError('--python currently only works with --full')
4912
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004913 # Dart's formatter does not have the nice property of only operating on
4914 # modified chunks, so hard code full.
4915 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004916 try:
4917 command = [dart_format.FindDartFmtToolInChromiumTree()]
4918 if not opts.dry_run and not opts.diff:
4919 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004920 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004921
ppi@chromium.org6593d932016-03-03 15:41:15 +00004922 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004923 if opts.dry_run and stdout:
4924 return_value = 2
4925 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07004926 print('Warning: Unable to check Dart code formatting. Dart SDK not '
4927 'found in this checkout. Files in other languages are still '
4928 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004929
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004930 # Format GN build files. Always run on full build files for canonical form.
4931 if gn_diff_files:
4932 cmd = ['gn', 'format']
4933 if not opts.dry_run and not opts.diff:
4934 cmd.append('--in-place')
4935 for gn_diff_file in gn_diff_files:
bsep@chromium.org627d9002016-04-29 00:00:52 +00004936 stdout = RunCommand(cmd + [gn_diff_file],
4937 shell=sys.platform == 'win32',
4938 cwd=top_dir)
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004939 if opts.diff:
4940 sys.stdout.write(stdout)
4941
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004942 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004943
4944
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004945@subcommand.usage('<codereview url or issue id>')
4946def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004947 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004948 _, args = parser.parse_args(args)
4949
4950 if len(args) != 1:
4951 parser.print_help()
4952 return 1
4953
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004954 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00004955 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004956 parser.print_help()
4957 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00004958 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004959
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004960 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00004961 output = RunGit(['config', '--local', '--get-regexp',
4962 r'branch\..*\.%s' % issueprefix],
4963 error_ok=True)
4964 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004965 if issue == target_issue:
4966 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004967
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004968 branches = []
4969 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00004970 branches.extend(find_issues(cls.IssueSettingSuffix()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004971 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07004972 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004973 return 1
4974 if len(branches) == 1:
4975 RunGit(['checkout', branches[0]])
4976 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004977 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004978 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07004979 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004980 which = raw_input('Choose by index: ')
4981 try:
4982 RunGit(['checkout', branches[int(which)]])
4983 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07004984 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004985 return 1
4986
4987 return 0
4988
4989
maruel@chromium.org29404b52014-09-08 22:58:00 +00004990def CMDlol(parser, args):
4991 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07004992 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00004993 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
4994 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
4995 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07004996 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00004997 return 0
4998
4999
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005000class OptionParser(optparse.OptionParser):
5001 """Creates the option parse and add --verbose support."""
5002 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005003 optparse.OptionParser.__init__(
5004 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005005 self.add_option(
5006 '-v', '--verbose', action='count', default=0,
5007 help='Use 2 times for more debugging info')
5008
5009 def parse_args(self, args=None, values=None):
5010 options, args = optparse.OptionParser.parse_args(self, args, values)
5011 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5012 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5013 return options, args
5014
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005015
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005016def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005017 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005018 print('\nYour python version %s is unsupported, please upgrade.\n' %
5019 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005020 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005021
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005022 # Reload settings.
5023 global settings
5024 settings = Settings()
5025
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005026 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005027 dispatcher = subcommand.CommandDispatcher(__name__)
5028 try:
5029 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005030 except auth.AuthenticationError as e:
5031 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005032 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005033 if e.code != 500:
5034 raise
5035 DieWithError(
5036 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5037 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005038 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005039
5040
5041if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005042 # These affect sys.stdout so do it outside of main() to simplify mocks in
5043 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005044 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005045 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005046 try:
5047 sys.exit(main(sys.argv[1:]))
5048 except KeyboardInterrupt:
5049 sys.stderr.write('interrupted\n')
5050 sys.exit(1)