blob: 638612e15914bd3f7645728943a55f5bd9c715f4 [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:
773 self.squash_gerrit_uploads = (
774 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
775 error_ok=True).strip() == 'true')
776 return self.squash_gerrit_uploads
777
tandrii@chromium.org28253532016-04-14 13:46:56 +0000778 def GetGerritSkipEnsureAuthenticated(self):
779 """Return True if EnsureAuthenticated should not be done for Gerrit
780 uploads."""
781 if self.gerrit_skip_ensure_authenticated is None:
782 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000783 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000784 error_ok=True).strip() == 'true')
785 return self.gerrit_skip_ensure_authenticated
786
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000787 def GetGitEditor(self):
788 """Return the editor specified in the git config, or None if none is."""
789 if self.git_editor is None:
790 self.git_editor = self._GetConfig('core.editor', error_ok=True)
791 return self.git_editor or None
792
thestig@chromium.org44202a22014-03-11 19:22:18 +0000793 def GetLintRegex(self):
794 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
795 DEFAULT_LINT_REGEX)
796
797 def GetLintIgnoreRegex(self):
798 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
799 DEFAULT_LINT_IGNORE_REGEX)
800
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000801 def GetProject(self):
802 if not self.project:
803 self.project = self._GetRietveldConfig('project', error_ok=True)
804 return self.project
805
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000806 def GetForceHttpsCommitUrl(self):
807 if not self.force_https_commit_url:
808 self.force_https_commit_url = self._GetRietveldConfig(
809 'force-https-commit-url', error_ok=True)
810 return self.force_https_commit_url
811
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000812 def GetPendingRefPrefix(self):
813 if not self.pending_ref_prefix:
814 self.pending_ref_prefix = self._GetRietveldConfig(
815 'pending-ref-prefix', error_ok=True)
816 return self.pending_ref_prefix
817
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000818 def _GetRietveldConfig(self, param, **kwargs):
819 return self._GetConfig('rietveld.' + param, **kwargs)
820
rmistry@google.com78948ed2015-07-08 23:09:57 +0000821 def _GetBranchConfig(self, branch_name, param, **kwargs):
822 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
823
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000824 def _GetConfig(self, param, **kwargs):
825 self.LazyUpdateIfNeeded()
826 return RunGit(['config', param], **kwargs).strip()
827
828
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000829def ShortBranchName(branch):
830 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000831 return branch.replace('refs/heads/', '', 1)
832
833
834def GetCurrentBranchRef():
835 """Returns branch ref (e.g., refs/heads/master) or None."""
836 return RunGit(['symbolic-ref', 'HEAD'],
837 stderr=subprocess2.VOID, error_ok=True).strip() or None
838
839
840def GetCurrentBranch():
841 """Returns current branch or None.
842
843 For refs/heads/* branches, returns just last part. For others, full ref.
844 """
845 branchref = GetCurrentBranchRef()
846 if branchref:
847 return ShortBranchName(branchref)
848 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000849
850
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000851class _CQState(object):
852 """Enum for states of CL with respect to Commit Queue."""
853 NONE = 'none'
854 DRY_RUN = 'dry_run'
855 COMMIT = 'commit'
856
857 ALL_STATES = [NONE, DRY_RUN, COMMIT]
858
859
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000860class _ParsedIssueNumberArgument(object):
861 def __init__(self, issue=None, patchset=None, hostname=None):
862 self.issue = issue
863 self.patchset = patchset
864 self.hostname = hostname
865
866 @property
867 def valid(self):
868 return self.issue is not None
869
870
871class _RietveldParsedIssueNumberArgument(_ParsedIssueNumberArgument):
872 def __init__(self, *args, **kwargs):
873 self.patch_url = kwargs.pop('patch_url', None)
874 super(_RietveldParsedIssueNumberArgument, self).__init__(*args, **kwargs)
875
876
877def ParseIssueNumberArgument(arg):
878 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
879 fail_result = _ParsedIssueNumberArgument()
880
881 if arg.isdigit():
882 return _ParsedIssueNumberArgument(issue=int(arg))
883 if not arg.startswith('http'):
884 return fail_result
885 url = gclient_utils.UpgradeToHttps(arg)
886 try:
887 parsed_url = urlparse.urlparse(url)
888 except ValueError:
889 return fail_result
890 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
891 tmp = cls.ParseIssueURL(parsed_url)
892 if tmp is not None:
893 return tmp
894 return fail_result
895
896
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000897class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000898 """Changelist works with one changelist in local branch.
899
900 Supports two codereview backends: Rietveld or Gerrit, selected at object
901 creation.
902
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +0000903 Notes:
904 * Not safe for concurrent multi-{thread,process} use.
905 * Caches values from current branch. Therefore, re-use after branch change
906 with care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000907 """
908
909 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
910 """Create a new ChangeList instance.
911
912 If issue is given, the codereview must be given too.
913
914 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
915 Otherwise, it's decided based on current configuration of the local branch,
916 with default being 'rietveld' for backwards compatibility.
917 See _load_codereview_impl for more details.
918
919 **kwargs will be passed directly to codereview implementation.
920 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000921 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000922 global settings
923 if not settings:
924 # Happens when git_cl.py is used as a utility library.
925 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000926
927 if issue:
928 assert codereview, 'codereview must be known, if issue is known'
929
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000930 self.branchref = branchref
931 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000932 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000933 self.branch = ShortBranchName(self.branchref)
934 else:
935 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000936 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000937 self.lookedup_issue = False
938 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000939 self.has_description = False
940 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000941 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000942 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000943 self.cc = None
944 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000945 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000946
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000947 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000948 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000949 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000950 assert self._codereview_impl
951 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000952
953 def _load_codereview_impl(self, codereview=None, **kwargs):
954 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000955 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
956 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
957 self._codereview = codereview
958 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000959 return
960
961 # Automatic selection based on issue number set for a current branch.
962 # Rietveld takes precedence over Gerrit.
963 assert not self.issue
964 # Whether we find issue or not, we are doing the lookup.
965 self.lookedup_issue = True
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000966 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000967 setting = cls.IssueSetting(self.GetBranch())
968 issue = RunGit(['config', setting], error_ok=True).strip()
969 if issue:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000970 self._codereview = codereview
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000971 self._codereview_impl = cls(self, **kwargs)
972 self.issue = int(issue)
973 return
974
975 # No issue is set for this branch, so decide based on repo-wide settings.
976 return self._load_codereview_impl(
977 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
978 **kwargs)
979
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000980 def IsGerrit(self):
981 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000982
983 def GetCCList(self):
984 """Return the users cc'd on this CL.
985
986 Return is a string suitable for passing to gcl with the --cc flag.
987 """
988 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000989 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000990 more_cc = ','.join(self.watchers)
991 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
992 return self.cc
993
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000994 def GetCCListWithoutDefault(self):
995 """Return the users cc'd on this CL excluding default ones."""
996 if self.cc is None:
997 self.cc = ','.join(self.watchers)
998 return self.cc
999
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001000 def SetWatchers(self, watchers):
1001 """Set the list of email addresses that should be cc'd based on the changed
1002 files in this CL.
1003 """
1004 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001005
1006 def GetBranch(self):
1007 """Returns the short branch name, e.g. 'master'."""
1008 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001009 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001010 if not branchref:
1011 return None
1012 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001013 self.branch = ShortBranchName(self.branchref)
1014 return self.branch
1015
1016 def GetBranchRef(self):
1017 """Returns the full branch name, e.g. 'refs/heads/master'."""
1018 self.GetBranch() # Poke the lazy loader.
1019 return self.branchref
1020
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001021 def ClearBranch(self):
1022 """Clears cached branch data of this object."""
1023 self.branch = self.branchref = None
1024
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001025 @staticmethod
1026 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001027 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001028 e.g. 'origin', 'refs/heads/master'
1029 """
1030 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001031 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
1032 error_ok=True).strip()
1033 if upstream_branch:
1034 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
1035 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001036 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1037 error_ok=True).strip()
1038 if upstream_branch:
1039 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001040 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001041 # Fall back on trying a git-svn upstream branch.
1042 if settings.GetIsGitSvn():
1043 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001044 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001045 # Else, try to guess the origin remote.
1046 remote_branches = RunGit(['branch', '-r']).split()
1047 if 'origin/master' in remote_branches:
1048 # Fall back on origin/master if it exits.
1049 remote = 'origin'
1050 upstream_branch = 'refs/heads/master'
1051 elif 'origin/trunk' in remote_branches:
1052 # Fall back on origin/trunk if it exists. Generally a shared
1053 # git-svn clone
1054 remote = 'origin'
1055 upstream_branch = 'refs/heads/trunk'
1056 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001057 DieWithError(
1058 'Unable to determine default branch to diff against.\n'
1059 'Either pass complete "git diff"-style arguments, like\n'
1060 ' git cl upload origin/master\n'
1061 'or verify this branch is set up to track another \n'
1062 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001063
1064 return remote, upstream_branch
1065
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001066 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001067 upstream_branch = self.GetUpstreamBranch()
1068 if not BranchExists(upstream_branch):
1069 DieWithError('The upstream for the current branch (%s) does not exist '
1070 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001071 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001072 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001073
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001074 def GetUpstreamBranch(self):
1075 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001076 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001077 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001078 upstream_branch = upstream_branch.replace('refs/heads/',
1079 'refs/remotes/%s/' % remote)
1080 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1081 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001082 self.upstream_branch = upstream_branch
1083 return self.upstream_branch
1084
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001085 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001086 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001087 remote, branch = None, self.GetBranch()
1088 seen_branches = set()
1089 while branch not in seen_branches:
1090 seen_branches.add(branch)
1091 remote, branch = self.FetchUpstreamTuple(branch)
1092 branch = ShortBranchName(branch)
1093 if remote != '.' or branch.startswith('refs/remotes'):
1094 break
1095 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001096 remotes = RunGit(['remote'], error_ok=True).split()
1097 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001098 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001099 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001100 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001101 logging.warning('Could not determine which remote this change is '
1102 'associated with, so defaulting to "%s". This may '
1103 'not be what you want. You may prevent this message '
1104 'by running "git svn info" as documented here: %s',
1105 self._remote,
1106 GIT_INSTRUCTIONS_URL)
1107 else:
1108 logging.warn('Could not determine which remote this change is '
1109 'associated with. You may prevent this message by '
1110 'running "git svn info" as documented here: %s',
1111 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001112 branch = 'HEAD'
1113 if branch.startswith('refs/remotes'):
1114 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001115 elif branch.startswith('refs/branch-heads/'):
1116 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001117 else:
1118 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001119 return self._remote
1120
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001121 def GitSanityChecks(self, upstream_git_obj):
1122 """Checks git repo status and ensures diff is from local commits."""
1123
sbc@chromium.org79706062015-01-14 21:18:12 +00001124 if upstream_git_obj is None:
1125 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001126 print('ERROR: unable to determine current branch (detached HEAD?)',
1127 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001128 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001129 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001130 return False
1131
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001132 # Verify the commit we're diffing against is in our current branch.
1133 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1134 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1135 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001136 print('ERROR: %s is not in the current branch. You may need to rebase '
1137 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001138 return False
1139
1140 # List the commits inside the diff, and verify they are all local.
1141 commits_in_diff = RunGit(
1142 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1143 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1144 remote_branch = remote_branch.strip()
1145 if code != 0:
1146 _, remote_branch = self.GetRemoteBranch()
1147
1148 commits_in_remote = RunGit(
1149 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1150
1151 common_commits = set(commits_in_diff) & set(commits_in_remote)
1152 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001153 print('ERROR: Your diff contains %d commits already in %s.\n'
1154 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1155 'the diff. If you are using a custom git flow, you can override'
1156 ' the reference used for this check with "git config '
1157 'gitcl.remotebranch <git-ref>".' % (
1158 len(common_commits), remote_branch, upstream_git_obj),
1159 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001160 return False
1161 return True
1162
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001163 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001164 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001165
1166 Returns None if it is not set.
1167 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001168 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
1169 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001170
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001171 def GetGitSvnRemoteUrl(self):
1172 """Return the configured git-svn remote URL parsed from git svn info.
1173
1174 Returns None if it is not set.
1175 """
1176 # URL is dependent on the current directory.
1177 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1178 if data:
1179 keys = dict(line.split(': ', 1) for line in data.splitlines()
1180 if ': ' in line)
1181 return keys.get('URL', None)
1182 return None
1183
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001184 def GetRemoteUrl(self):
1185 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1186
1187 Returns None if there is no remote.
1188 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001189 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001190 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1191
1192 # If URL is pointing to a local directory, it is probably a git cache.
1193 if os.path.isdir(url):
1194 url = RunGit(['config', 'remote.%s.url' % remote],
1195 error_ok=True,
1196 cwd=url).strip()
1197 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001198
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001199 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001200 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001201 if self.issue is None and not self.lookedup_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001202 issue = RunGit(['config',
1203 self._codereview_impl.IssueSetting(self.GetBranch())],
1204 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001205 self.issue = int(issue) or None if issue else None
1206 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001207 return self.issue
1208
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001209 def GetIssueURL(self):
1210 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001211 issue = self.GetIssue()
1212 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001213 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001214 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001215
1216 def GetDescription(self, pretty=False):
1217 if not self.has_description:
1218 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001219 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001220 self.has_description = True
1221 if pretty:
1222 wrapper = textwrap.TextWrapper()
1223 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1224 return wrapper.fill(self.description)
1225 return self.description
1226
1227 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001228 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001229 if self.patchset is None and not self.lookedup_patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001230 patchset = RunGit(['config', self._codereview_impl.PatchsetSetting()],
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001231 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001232 self.patchset = int(patchset) or None if patchset else None
1233 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001234 return self.patchset
1235
1236 def SetPatchset(self, patchset):
1237 """Set this branch's patchset. If patchset=0, clears the patchset."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001238 patchset_setting = self._codereview_impl.PatchsetSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001239 if patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001240 RunGit(['config', patchset_setting, str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001241 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001242 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001243 RunGit(['config', '--unset', patchset_setting],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001244 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001245 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001246
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001247 def SetIssue(self, issue=None):
1248 """Set this branch's issue. If issue isn't given, clears the issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001249 issue_setting = self._codereview_impl.IssueSetting(self.GetBranch())
1250 codereview_setting = self._codereview_impl.GetCodereviewServerSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001251 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001252 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001253 RunGit(['config', issue_setting, str(issue)])
1254 codereview_server = self._codereview_impl.GetCodereviewServer()
1255 if codereview_server:
1256 RunGit(['config', codereview_setting, codereview_server])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001257 else:
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001258 # Reset it regardless. It doesn't hurt.
1259 config_settings = [issue_setting, self._codereview_impl.PatchsetSetting()]
1260 for prop in (['last-upload-hash'] +
1261 self._codereview_impl._PostUnsetIssueProperties()):
1262 config_settings.append('branch.%s.%s' % (self.GetBranch(), prop))
1263 for setting in config_settings:
1264 RunGit(['config', '--unset', setting], error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001265 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001266 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001267
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001268 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001269 if not self.GitSanityChecks(upstream_branch):
1270 DieWithError('\nGit sanity check failure')
1271
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001272 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001273 if not root:
1274 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001275 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001276
1277 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001278 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001279 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001280 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001281 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001282 except subprocess2.CalledProcessError:
1283 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001284 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001285 'This branch probably doesn\'t exist anymore. To reset the\n'
1286 'tracking branch, please run\n'
1287 ' git branch --set-upstream %s trunk\n'
1288 'replacing trunk with origin/master or the relevant branch') %
1289 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001290
maruel@chromium.org52424302012-08-29 15:14:30 +00001291 issue = self.GetIssue()
1292 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001293 if issue:
1294 description = self.GetDescription()
1295 else:
1296 # If the change was never uploaded, use the log messages of all commits
1297 # up to the branch point, as git cl upload will prefill the description
1298 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001299 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1300 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001301
1302 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001303 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001304 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001305 name,
1306 description,
1307 absroot,
1308 files,
1309 issue,
1310 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001311 author,
1312 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001313
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001314 def UpdateDescription(self, description):
1315 self.description = description
1316 return self._codereview_impl.UpdateDescriptionRemote(description)
1317
1318 def RunHook(self, committing, may_prompt, verbose, change):
1319 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1320 try:
1321 return presubmit_support.DoPresubmitChecks(change, committing,
1322 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1323 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001324 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1325 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001326 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001327 DieWithError(
1328 ('%s\nMaybe your depot_tools is out of date?\n'
1329 'If all fails, contact maruel@') % e)
1330
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001331 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1332 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001333 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1334 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001335 else:
1336 # Assume url.
1337 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1338 urlparse.urlparse(issue_arg))
1339 if not parsed_issue_arg or not parsed_issue_arg.valid:
1340 DieWithError('Failed to parse issue argument "%s". '
1341 'Must be an issue number or a valid URL.' % issue_arg)
1342 return self._codereview_impl.CMDPatchWithParsedIssue(
1343 parsed_issue_arg, reject, nocommit, directory)
1344
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001345 def CMDUpload(self, options, git_diff_args, orig_args):
1346 """Uploads a change to codereview."""
1347 if git_diff_args:
1348 # TODO(ukai): is it ok for gerrit case?
1349 base_branch = git_diff_args[0]
1350 else:
1351 if self.GetBranch() is None:
1352 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1353
1354 # Default to diffing against common ancestor of upstream branch
1355 base_branch = self.GetCommonAncestorWithUpstream()
1356 git_diff_args = [base_branch, 'HEAD']
1357
1358 # Make sure authenticated to codereview before running potentially expensive
1359 # hooks. It is a fast, best efforts check. Codereview still can reject the
1360 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001361 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001362
1363 # Apply watchlists on upload.
1364 change = self.GetChange(base_branch, None)
1365 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1366 files = [f.LocalPath() for f in change.AffectedFiles()]
1367 if not options.bypass_watchlists:
1368 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1369
1370 if not options.bypass_hooks:
1371 if options.reviewers or options.tbr_owners:
1372 # Set the reviewer list now so that presubmit checks can access it.
1373 change_description = ChangeDescription(change.FullDescriptionText())
1374 change_description.update_reviewers(options.reviewers,
1375 options.tbr_owners,
1376 change)
1377 change.SetDescriptionText(change_description.description)
1378 hook_results = self.RunHook(committing=False,
1379 may_prompt=not options.force,
1380 verbose=options.verbose,
1381 change=change)
1382 if not hook_results.should_continue():
1383 return 1
1384 if not options.reviewers and hook_results.reviewers:
1385 options.reviewers = hook_results.reviewers.split(',')
1386
1387 if self.GetIssue():
1388 latest_patchset = self.GetMostRecentPatchset()
1389 local_patchset = self.GetPatchset()
1390 if (latest_patchset and local_patchset and
1391 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001392 print('The last upload made from this repository was patchset #%d but '
1393 'the most recent patchset on the server is #%d.'
1394 % (local_patchset, latest_patchset))
1395 print('Uploading will still work, but if you\'ve uploaded to this '
1396 'issue from another machine or branch the patch you\'re '
1397 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001398 ask_for_data('About to upload; enter to confirm.')
1399
1400 print_stats(options.similarity, options.find_copies, git_diff_args)
1401 ret = self.CMDUploadChange(options, git_diff_args, change)
1402 if not ret:
1403 git_set_branch_value('last-upload-hash',
1404 RunGit(['rev-parse', 'HEAD']).strip())
1405 # Run post upload hooks, if specified.
1406 if settings.GetRunPostUploadHook():
1407 presubmit_support.DoPostUploadExecuter(
1408 change,
1409 self,
1410 settings.GetRoot(),
1411 options.verbose,
1412 sys.stdout)
1413
1414 # Upload all dependencies if specified.
1415 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001416 print()
1417 print('--dependencies has been specified.')
1418 print('All dependent local branches will be re-uploaded.')
1419 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001420 # Remove the dependencies flag from args so that we do not end up in a
1421 # loop.
1422 orig_args.remove('--dependencies')
1423 ret = upload_branch_deps(self, orig_args)
1424 return ret
1425
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001426 def SetCQState(self, new_state):
1427 """Update the CQ state for latest patchset.
1428
1429 Issue must have been already uploaded and known.
1430 """
1431 assert new_state in _CQState.ALL_STATES
1432 assert self.GetIssue()
1433 return self._codereview_impl.SetCQState(new_state)
1434
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001435 # Forward methods to codereview specific implementation.
1436
1437 def CloseIssue(self):
1438 return self._codereview_impl.CloseIssue()
1439
1440 def GetStatus(self):
1441 return self._codereview_impl.GetStatus()
1442
1443 def GetCodereviewServer(self):
1444 return self._codereview_impl.GetCodereviewServer()
1445
1446 def GetApprovingReviewers(self):
1447 return self._codereview_impl.GetApprovingReviewers()
1448
1449 def GetMostRecentPatchset(self):
1450 return self._codereview_impl.GetMostRecentPatchset()
1451
1452 def __getattr__(self, attr):
1453 # This is because lots of untested code accesses Rietveld-specific stuff
1454 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001455 # on a case by case basis.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001456 return getattr(self._codereview_impl, attr)
1457
1458
1459class _ChangelistCodereviewBase(object):
1460 """Abstract base class encapsulating codereview specifics of a changelist."""
1461 def __init__(self, changelist):
1462 self._changelist = changelist # instance of Changelist
1463
1464 def __getattr__(self, attr):
1465 # Forward methods to changelist.
1466 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1467 # _RietveldChangelistImpl to avoid this hack?
1468 return getattr(self._changelist, attr)
1469
1470 def GetStatus(self):
1471 """Apply a rough heuristic to give a simple summary of an issue's review
1472 or CQ status, assuming adherence to a common workflow.
1473
1474 Returns None if no issue for this branch, or specific string keywords.
1475 """
1476 raise NotImplementedError()
1477
1478 def GetCodereviewServer(self):
1479 """Returns server URL without end slash, like "https://codereview.com"."""
1480 raise NotImplementedError()
1481
1482 def FetchDescription(self):
1483 """Fetches and returns description from the codereview server."""
1484 raise NotImplementedError()
1485
1486 def GetCodereviewServerSetting(self):
1487 """Returns git config setting for the codereview server."""
1488 raise NotImplementedError()
1489
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001490 @classmethod
1491 def IssueSetting(cls, branch):
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001492 return 'branch.%s.%s' % (branch, cls.IssueSettingSuffix())
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001493
1494 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001495 def IssueSettingSuffix(cls):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001496 """Returns name of git config setting which stores issue number for a given
1497 branch."""
1498 raise NotImplementedError()
1499
1500 def PatchsetSetting(self):
1501 """Returns name of git config setting which stores issue number."""
1502 raise NotImplementedError()
1503
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001504 def _PostUnsetIssueProperties(self):
1505 """Which branch-specific properties to erase when unsettin issue."""
1506 raise NotImplementedError()
1507
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001508 def GetRieveldObjForPresubmit(self):
1509 # This is an unfortunate Rietveld-embeddedness in presubmit.
1510 # For non-Rietveld codereviews, this probably should return a dummy object.
1511 raise NotImplementedError()
1512
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001513 def GetGerritObjForPresubmit(self):
1514 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1515 return None
1516
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001517 def UpdateDescriptionRemote(self, description):
1518 """Update the description on codereview site."""
1519 raise NotImplementedError()
1520
1521 def CloseIssue(self):
1522 """Closes the issue."""
1523 raise NotImplementedError()
1524
1525 def GetApprovingReviewers(self):
1526 """Returns a list of reviewers approving the change.
1527
1528 Note: not necessarily committers.
1529 """
1530 raise NotImplementedError()
1531
1532 def GetMostRecentPatchset(self):
1533 """Returns the most recent patchset number from the codereview site."""
1534 raise NotImplementedError()
1535
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001536 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1537 directory):
1538 """Fetches and applies the issue.
1539
1540 Arguments:
1541 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1542 reject: if True, reject the failed patch instead of switching to 3-way
1543 merge. Rietveld only.
1544 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1545 only.
1546 directory: switch to directory before applying the patch. Rietveld only.
1547 """
1548 raise NotImplementedError()
1549
1550 @staticmethod
1551 def ParseIssueURL(parsed_url):
1552 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1553 failed."""
1554 raise NotImplementedError()
1555
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001556 def EnsureAuthenticated(self, force):
1557 """Best effort check that user is authenticated with codereview server.
1558
1559 Arguments:
1560 force: whether to skip confirmation questions.
1561 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001562 raise NotImplementedError()
1563
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001564 def CMDUploadChange(self, options, args, change):
1565 """Uploads a change to codereview."""
1566 raise NotImplementedError()
1567
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001568 def SetCQState(self, new_state):
1569 """Update the CQ state for latest patchset.
1570
1571 Issue must have been already uploaded and known.
1572 """
1573 raise NotImplementedError()
1574
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001575
1576class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1577 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1578 super(_RietveldChangelistImpl, self).__init__(changelist)
1579 assert settings, 'must be initialized in _ChangelistCodereviewBase'
1580 settings.GetDefaultServerUrl()
1581
1582 self._rietveld_server = rietveld_server
1583 self._auth_config = auth_config
1584 self._props = None
1585 self._rpc_server = None
1586
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001587 def GetCodereviewServer(self):
1588 if not self._rietveld_server:
1589 # If we're on a branch then get the server potentially associated
1590 # with that branch.
1591 if self.GetIssue():
1592 rietveld_server_setting = self.GetCodereviewServerSetting()
1593 if rietveld_server_setting:
1594 self._rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1595 ['config', rietveld_server_setting], error_ok=True).strip())
1596 if not self._rietveld_server:
1597 self._rietveld_server = settings.GetDefaultServerUrl()
1598 return self._rietveld_server
1599
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001600 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001601 """Best effort check that user is authenticated with Rietveld server."""
1602 if self._auth_config.use_oauth2:
1603 authenticator = auth.get_authenticator_for_host(
1604 self.GetCodereviewServer(), self._auth_config)
1605 if not authenticator.has_cached_credentials():
1606 raise auth.LoginRequiredError(self.GetCodereviewServer())
1607
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001608 def FetchDescription(self):
1609 issue = self.GetIssue()
1610 assert issue
1611 try:
1612 return self.RpcServer().get_description(issue).strip()
1613 except urllib2.HTTPError as e:
1614 if e.code == 404:
1615 DieWithError(
1616 ('\nWhile fetching the description for issue %d, received a '
1617 '404 (not found)\n'
1618 'error. It is likely that you deleted this '
1619 'issue on the server. If this is the\n'
1620 'case, please run\n\n'
1621 ' git cl issue 0\n\n'
1622 'to clear the association with the deleted issue. Then run '
1623 'this command again.') % issue)
1624 else:
1625 DieWithError(
1626 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1627 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001628 print('Warning: Failed to retrieve CL description due to network '
1629 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001630 return ''
1631
1632 def GetMostRecentPatchset(self):
1633 return self.GetIssueProperties()['patchsets'][-1]
1634
1635 def GetPatchSetDiff(self, issue, patchset):
1636 return self.RpcServer().get(
1637 '/download/issue%s_%s.diff' % (issue, patchset))
1638
1639 def GetIssueProperties(self):
1640 if self._props is None:
1641 issue = self.GetIssue()
1642 if not issue:
1643 self._props = {}
1644 else:
1645 self._props = self.RpcServer().get_issue_properties(issue, True)
1646 return self._props
1647
1648 def GetApprovingReviewers(self):
1649 return get_approving_reviewers(self.GetIssueProperties())
1650
1651 def AddComment(self, message):
1652 return self.RpcServer().add_comment(self.GetIssue(), message)
1653
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001654 def GetStatus(self):
1655 """Apply a rough heuristic to give a simple summary of an issue's review
1656 or CQ status, assuming adherence to a common workflow.
1657
1658 Returns None if no issue for this branch, or one of the following keywords:
1659 * 'error' - error from review tool (including deleted issues)
1660 * 'unsent' - not sent for review
1661 * 'waiting' - waiting for review
1662 * 'reply' - waiting for owner to reply to review
1663 * 'lgtm' - LGTM from at least one approved reviewer
1664 * 'commit' - in the commit queue
1665 * 'closed' - closed
1666 """
1667 if not self.GetIssue():
1668 return None
1669
1670 try:
1671 props = self.GetIssueProperties()
1672 except urllib2.HTTPError:
1673 return 'error'
1674
1675 if props.get('closed'):
1676 # Issue is closed.
1677 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001678 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001679 # Issue is in the commit queue.
1680 return 'commit'
1681
1682 try:
1683 reviewers = self.GetApprovingReviewers()
1684 except urllib2.HTTPError:
1685 return 'error'
1686
1687 if reviewers:
1688 # Was LGTM'ed.
1689 return 'lgtm'
1690
1691 messages = props.get('messages') or []
1692
1693 if not messages:
1694 # No message was sent.
1695 return 'unsent'
1696 if messages[-1]['sender'] != props.get('owner_email'):
1697 # Non-LGTM reply from non-owner
1698 return 'reply'
1699 return 'waiting'
1700
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001701 def UpdateDescriptionRemote(self, description):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001702 return self.RpcServer().update_description(
1703 self.GetIssue(), self.description)
1704
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001705 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001706 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001707
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001708 def SetFlag(self, flag, value):
1709 """Patchset must match."""
1710 if not self.GetPatchset():
1711 DieWithError('The patchset needs to match. Send another patchset.')
1712 try:
1713 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001714 self.GetIssue(), self.GetPatchset(), flag, value)
vapierfd77ac72016-06-16 08:33:57 -07001715 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001716 if e.code == 404:
1717 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1718 if e.code == 403:
1719 DieWithError(
1720 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1721 'match?') % (self.GetIssue(), self.GetPatchset()))
1722 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001723
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001724 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001725 """Returns an upload.RpcServer() to access this review's rietveld instance.
1726 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001727 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001728 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001729 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001730 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001731 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001732
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001733 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001734 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001735 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001736
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001737 def PatchsetSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001738 """Return the git setting that stores this change's most recent patchset."""
1739 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1740
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001741 def GetCodereviewServerSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001742 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001743 branch = self.GetBranch()
1744 if branch:
1745 return 'branch.%s.rietveldserver' % branch
1746 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001747
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001748 def _PostUnsetIssueProperties(self):
1749 """Which branch-specific properties to erase when unsetting issue."""
1750 return ['rietveldserver']
1751
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001752 def GetRieveldObjForPresubmit(self):
1753 return self.RpcServer()
1754
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001755 def SetCQState(self, new_state):
1756 props = self.GetIssueProperties()
1757 if props.get('private'):
1758 DieWithError('Cannot set-commit on private issue')
1759
1760 if new_state == _CQState.COMMIT:
1761 self.SetFlag('commit', '1')
1762 elif new_state == _CQState.NONE:
1763 self.SetFlag('commit', '0')
1764 else:
1765 raise NotImplementedError()
1766
1767
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001768 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1769 directory):
1770 # TODO(maruel): Use apply_issue.py
1771
1772 # PatchIssue should never be called with a dirty tree. It is up to the
1773 # caller to check this, but just in case we assert here since the
1774 # consequences of the caller not checking this could be dire.
1775 assert(not git_common.is_dirty_git_tree('apply'))
1776 assert(parsed_issue_arg.valid)
1777 self._changelist.issue = parsed_issue_arg.issue
1778 if parsed_issue_arg.hostname:
1779 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1780
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001781 if (isinstance(parsed_issue_arg, _RietveldParsedIssueNumberArgument) and
1782 parsed_issue_arg.patch_url):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001783 assert parsed_issue_arg.patchset
1784 patchset = parsed_issue_arg.patchset
1785 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1786 else:
1787 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1788 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1789
1790 # Switch up to the top-level directory, if necessary, in preparation for
1791 # applying the patch.
1792 top = settings.GetRelativeRoot()
1793 if top:
1794 os.chdir(top)
1795
1796 # Git patches have a/ at the beginning of source paths. We strip that out
1797 # with a sed script rather than the -p flag to patch so we can feed either
1798 # Git or svn-style patches into the same apply command.
1799 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1800 try:
1801 patch_data = subprocess2.check_output(
1802 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1803 except subprocess2.CalledProcessError:
1804 DieWithError('Git patch mungling failed.')
1805 logging.info(patch_data)
1806
1807 # We use "git apply" to apply the patch instead of "patch" so that we can
1808 # pick up file adds.
1809 # The --index flag means: also insert into the index (so we catch adds).
1810 cmd = ['git', 'apply', '--index', '-p0']
1811 if directory:
1812 cmd.extend(('--directory', directory))
1813 if reject:
1814 cmd.append('--reject')
1815 elif IsGitVersionAtLeast('1.7.12'):
1816 cmd.append('--3way')
1817 try:
1818 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
1819 stdin=patch_data, stdout=subprocess2.VOID)
1820 except subprocess2.CalledProcessError:
vapiera7fbd5a2016-06-16 09:17:49 -07001821 print('Failed to apply the patch')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001822 return 1
1823
1824 # If we had an issue, commit the current state and register the issue.
1825 if not nocommit:
1826 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
1827 'patch from issue %(i)s at patchset '
1828 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
1829 % {'i': self.GetIssue(), 'p': patchset})])
1830 self.SetIssue(self.GetIssue())
1831 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07001832 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001833 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001834 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001835 return 0
1836
1837 @staticmethod
1838 def ParseIssueURL(parsed_url):
1839 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1840 return None
1841 # Typical url: https://domain/<issue_number>[/[other]]
1842 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
1843 if match:
1844 return _RietveldParsedIssueNumberArgument(
1845 issue=int(match.group(1)),
1846 hostname=parsed_url.netloc)
1847 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
1848 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
1849 if match:
1850 return _RietveldParsedIssueNumberArgument(
1851 issue=int(match.group(1)),
1852 patchset=int(match.group(2)),
1853 hostname=parsed_url.netloc,
1854 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
1855 return None
1856
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001857 def CMDUploadChange(self, options, args, change):
1858 """Upload the patch to Rietveld."""
1859 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1860 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001861 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
1862 if options.emulate_svn_auto_props:
1863 upload_args.append('--emulate_svn_auto_props')
1864
1865 change_desc = None
1866
1867 if options.email is not None:
1868 upload_args.extend(['--email', options.email])
1869
1870 if self.GetIssue():
1871 if options.title:
1872 upload_args.extend(['--title', options.title])
1873 if options.message:
1874 upload_args.extend(['--message', options.message])
1875 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07001876 print('This branch is associated with issue %s. '
1877 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001878 else:
1879 if options.title:
1880 upload_args.extend(['--title', options.title])
1881 message = (options.title or options.message or
1882 CreateDescriptionFromLog(args))
1883 change_desc = ChangeDescription(message)
1884 if options.reviewers or options.tbr_owners:
1885 change_desc.update_reviewers(options.reviewers,
1886 options.tbr_owners,
1887 change)
1888 if not options.force:
1889 change_desc.prompt()
1890
1891 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07001892 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001893 return 1
1894
1895 upload_args.extend(['--message', change_desc.description])
1896 if change_desc.get_reviewers():
1897 upload_args.append('--reviewers=%s' % ','.join(
1898 change_desc.get_reviewers()))
1899 if options.send_mail:
1900 if not change_desc.get_reviewers():
1901 DieWithError("Must specify reviewers to send email.")
1902 upload_args.append('--send_mail')
1903
1904 # We check this before applying rietveld.private assuming that in
1905 # rietveld.cc only addresses which we can send private CLs to are listed
1906 # if rietveld.private is set, and so we should ignore rietveld.cc only
1907 # when --private is specified explicitly on the command line.
1908 if options.private:
1909 logging.warn('rietveld.cc is ignored since private flag is specified. '
1910 'You need to review and add them manually if necessary.')
1911 cc = self.GetCCListWithoutDefault()
1912 else:
1913 cc = self.GetCCList()
1914 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
1915 if cc:
1916 upload_args.extend(['--cc', cc])
1917
1918 if options.private or settings.GetDefaultPrivateFlag() == "True":
1919 upload_args.append('--private')
1920
1921 upload_args.extend(['--git_similarity', str(options.similarity)])
1922 if not options.find_copies:
1923 upload_args.extend(['--git_no_find_copies'])
1924
1925 # Include the upstream repo's URL in the change -- this is useful for
1926 # projects that have their source spread across multiple repos.
1927 remote_url = self.GetGitBaseUrlFromConfig()
1928 if not remote_url:
1929 if settings.GetIsGitSvn():
1930 remote_url = self.GetGitSvnRemoteUrl()
1931 else:
1932 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
1933 remote_url = '%s@%s' % (self.GetRemoteUrl(),
1934 self.GetUpstreamBranch().split('/')[-1])
1935 if remote_url:
1936 upload_args.extend(['--base_url', remote_url])
1937 remote, remote_branch = self.GetRemoteBranch()
1938 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
1939 settings.GetPendingRefPrefix())
1940 if target_ref:
1941 upload_args.extend(['--target_ref', target_ref])
1942
1943 # Look for dependent patchsets. See crbug.com/480453 for more details.
1944 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
1945 upstream_branch = ShortBranchName(upstream_branch)
1946 if remote is '.':
1947 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001948 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001949 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07001950 print()
1951 print('Skipping dependency patchset upload because git config '
1952 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
1953 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001954 else:
1955 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001956 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001957 auth_config=auth_config)
1958 branch_cl_issue_url = branch_cl.GetIssueURL()
1959 branch_cl_issue = branch_cl.GetIssue()
1960 branch_cl_patchset = branch_cl.GetPatchset()
1961 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
1962 upload_args.extend(
1963 ['--depends_on_patchset', '%s:%s' % (
1964 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001965 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001966 '\n'
1967 'The current branch (%s) is tracking a local branch (%s) with '
1968 'an associated CL.\n'
1969 'Adding %s/#ps%s as a dependency patchset.\n'
1970 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
1971 branch_cl_patchset))
1972
1973 project = settings.GetProject()
1974 if project:
1975 upload_args.extend(['--project', project])
1976
1977 if options.cq_dry_run:
1978 upload_args.extend(['--cq_dry_run'])
1979
1980 try:
1981 upload_args = ['upload'] + upload_args + args
1982 logging.info('upload.RealMain(%s)', upload_args)
1983 issue, patchset = upload.RealMain(upload_args)
1984 issue = int(issue)
1985 patchset = int(patchset)
1986 except KeyboardInterrupt:
1987 sys.exit(1)
1988 except:
1989 # If we got an exception after the user typed a description for their
1990 # change, back up the description before re-raising.
1991 if change_desc:
1992 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1993 print('\nGot exception while uploading -- saving description to %s\n' %
1994 backup_path)
1995 backup_file = open(backup_path, 'w')
1996 backup_file.write(change_desc.description)
1997 backup_file.close()
1998 raise
1999
2000 if not self.GetIssue():
2001 self.SetIssue(issue)
2002 self.SetPatchset(patchset)
2003
2004 if options.use_commit_queue:
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002005 self.SetCQState(_CQState.COMMIT)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002006 return 0
2007
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002008
2009class _GerritChangelistImpl(_ChangelistCodereviewBase):
2010 def __init__(self, changelist, auth_config=None):
2011 # auth_config is Rietveld thing, kept here to preserve interface only.
2012 super(_GerritChangelistImpl, self).__init__(changelist)
2013 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002014 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002015 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002016 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002017
2018 def _GetGerritHost(self):
2019 # Lazy load of configs.
2020 self.GetCodereviewServer()
2021 return self._gerrit_host
2022
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002023 def _GetGitHost(self):
2024 """Returns git host to be used when uploading change to Gerrit."""
2025 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2026
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002027 def GetCodereviewServer(self):
2028 if not self._gerrit_server:
2029 # If we're on a branch then get the server potentially associated
2030 # with that branch.
2031 if self.GetIssue():
2032 gerrit_server_setting = self.GetCodereviewServerSetting()
2033 if gerrit_server_setting:
2034 self._gerrit_server = RunGit(['config', gerrit_server_setting],
2035 error_ok=True).strip()
2036 if self._gerrit_server:
2037 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
2038 if not self._gerrit_server:
2039 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2040 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002041 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002042 parts[0] = parts[0] + '-review'
2043 self._gerrit_host = '.'.join(parts)
2044 self._gerrit_server = 'https://%s' % self._gerrit_host
2045 return self._gerrit_server
2046
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002047 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00002048 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002049 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002050
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002051 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002052 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002053 if settings.GetGerritSkipEnsureAuthenticated():
2054 # For projects with unusual authentication schemes.
2055 # See http://crbug.com/603378.
2056 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002057 # Lazy-loader to identify Gerrit and Git hosts.
2058 if gerrit_util.GceAuthenticator.is_gce():
2059 return
2060 self.GetCodereviewServer()
2061 git_host = self._GetGitHost()
2062 assert self._gerrit_server and self._gerrit_host
2063 cookie_auth = gerrit_util.CookiesAuthenticator()
2064
2065 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2066 git_auth = cookie_auth.get_auth_header(git_host)
2067 if gerrit_auth and git_auth:
2068 if gerrit_auth == git_auth:
2069 return
2070 print((
2071 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2072 ' Check your %s or %s file for credentials of hosts:\n'
2073 ' %s\n'
2074 ' %s\n'
2075 ' %s') %
2076 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2077 git_host, self._gerrit_host,
2078 cookie_auth.get_new_password_message(git_host)))
2079 if not force:
2080 ask_for_data('If you know what you are doing, press Enter to continue, '
2081 'Ctrl+C to abort.')
2082 return
2083 else:
2084 missing = (
2085 [] if gerrit_auth else [self._gerrit_host] +
2086 [] if git_auth else [git_host])
2087 DieWithError('Credentials for the following hosts are required:\n'
2088 ' %s\n'
2089 'These are read from %s (or legacy %s)\n'
2090 '%s' % (
2091 '\n '.join(missing),
2092 cookie_auth.get_gitcookies_path(),
2093 cookie_auth.get_netrc_path(),
2094 cookie_auth.get_new_password_message(git_host)))
2095
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002096
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002097 def PatchsetSetting(self):
2098 """Return the git setting that stores this change's most recent patchset."""
2099 return 'branch.%s.gerritpatchset' % self.GetBranch()
2100
2101 def GetCodereviewServerSetting(self):
2102 """Returns the git setting that stores this change's Gerrit server."""
2103 branch = self.GetBranch()
2104 if branch:
2105 return 'branch.%s.gerritserver' % branch
2106 return None
2107
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002108 def _PostUnsetIssueProperties(self):
2109 """Which branch-specific properties to erase when unsetting issue."""
2110 return [
2111 'gerritserver',
2112 'gerritsquashhash',
2113 ]
2114
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002115 def GetRieveldObjForPresubmit(self):
2116 class ThisIsNotRietveldIssue(object):
2117 def __nonzero__(self):
2118 # This is a hack to make presubmit_support think that rietveld is not
2119 # defined, yet still ensure that calls directly result in a decent
2120 # exception message below.
2121 return False
2122
2123 def __getattr__(self, attr):
2124 print(
2125 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2126 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2127 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2128 'or use Rietveld for codereview.\n'
2129 'See also http://crbug.com/579160.' % attr)
2130 raise NotImplementedError()
2131 return ThisIsNotRietveldIssue()
2132
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002133 def GetGerritObjForPresubmit(self):
2134 return presubmit_support.GerritAccessor(self._GetGerritHost())
2135
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002136 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002137 """Apply a rough heuristic to give a simple summary of an issue's review
2138 or CQ status, assuming adherence to a common workflow.
2139
2140 Returns None if no issue for this branch, or one of the following keywords:
2141 * 'error' - error from review tool (including deleted issues)
2142 * 'unsent' - no reviewers added
2143 * 'waiting' - waiting for review
2144 * 'reply' - waiting for owner to reply to review
2145 * 'not lgtm' - Code-Review -2 from at least one approved reviewer
2146 * 'lgtm' - Code-Review +2 from at least one approved reviewer
2147 * 'commit' - in the commit queue
2148 * 'closed' - abandoned
2149 """
2150 if not self.GetIssue():
2151 return None
2152
2153 try:
2154 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
2155 except httplib.HTTPException:
2156 return 'error'
2157
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002158 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002159 return 'closed'
2160
2161 cq_label = data['labels'].get('Commit-Queue', {})
2162 if cq_label:
2163 # Vote value is a stringified integer, which we expect from 0 to 2.
2164 vote_value = cq_label.get('value', '0')
2165 vote_text = cq_label.get('values', {}).get(vote_value, '')
2166 if vote_text.lower() == 'commit':
2167 return 'commit'
2168
2169 lgtm_label = data['labels'].get('Code-Review', {})
2170 if lgtm_label:
2171 if 'rejected' in lgtm_label:
2172 return 'not lgtm'
2173 if 'approved' in lgtm_label:
2174 return 'lgtm'
2175
2176 if not data.get('reviewers', {}).get('REVIEWER', []):
2177 return 'unsent'
2178
2179 messages = data.get('messages', [])
2180 if messages:
2181 owner = data['owner'].get('_account_id')
2182 last_message_author = messages[-1].get('author', {}).get('_account_id')
2183 if owner != last_message_author:
2184 # Some reply from non-owner.
2185 return 'reply'
2186
2187 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002188
2189 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002190 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002191 return data['revisions'][data['current_revision']]['_number']
2192
2193 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002194 data = self._GetChangeDetail(['CURRENT_REVISION'])
2195 current_rev = data['current_revision']
2196 url = data['revisions'][current_rev]['fetch']['http']['url']
2197 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002198
2199 def UpdateDescriptionRemote(self, description):
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002200 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
2201 description)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002202
2203 def CloseIssue(self):
2204 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2205
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002206 def GetApprovingReviewers(self):
2207 """Returns a list of reviewers approving the change.
2208
2209 Note: not necessarily committers.
2210 """
2211 raise NotImplementedError()
2212
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002213 def SubmitIssue(self, wait_for_merge=True):
2214 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2215 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002216
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002217 def _GetChangeDetail(self, options=None, issue=None):
2218 options = options or []
2219 issue = issue or self.GetIssue()
2220 assert issue, 'issue required to query Gerrit'
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002221 return gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2222 options)
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002223
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002224 def CMDLand(self, force, bypass_hooks, verbose):
2225 if git_common.is_dirty_git_tree('land'):
2226 return 1
2227 differs = True
2228 last_upload = RunGit(['config',
2229 'branch.%s.gerritsquashhash' % self.GetBranch()],
2230 error_ok=True).strip()
2231 # Note: git diff outputs nothing if there is no diff.
2232 if not last_upload or RunGit(['diff', last_upload]).strip():
2233 print('WARNING: some changes from local branch haven\'t been uploaded')
2234 else:
2235 detail = self._GetChangeDetail(['CURRENT_REVISION'])
2236 if detail['current_revision'] == last_upload:
2237 differs = False
2238 else:
2239 print('WARNING: local branch contents differ from latest uploaded '
2240 'patchset')
2241 if differs:
2242 if not force:
2243 ask_for_data(
2244 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2245 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2246 elif not bypass_hooks:
2247 hook_results = self.RunHook(
2248 committing=True,
2249 may_prompt=not force,
2250 verbose=verbose,
2251 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2252 if not hook_results.should_continue():
2253 return 1
2254
2255 self.SubmitIssue(wait_for_merge=True)
2256 print('Issue %s has been submitted.' % self.GetIssueURL())
2257 return 0
2258
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002259 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2260 directory):
2261 assert not reject
2262 assert not nocommit
2263 assert not directory
2264 assert parsed_issue_arg.valid
2265
2266 self._changelist.issue = parsed_issue_arg.issue
2267
2268 if parsed_issue_arg.hostname:
2269 self._gerrit_host = parsed_issue_arg.hostname
2270 self._gerrit_server = 'https://%s' % self._gerrit_host
2271
2272 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2273
2274 if not parsed_issue_arg.patchset:
2275 # Use current revision by default.
2276 revision_info = detail['revisions'][detail['current_revision']]
2277 patchset = int(revision_info['_number'])
2278 else:
2279 patchset = parsed_issue_arg.patchset
2280 for revision_info in detail['revisions'].itervalues():
2281 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2282 break
2283 else:
2284 DieWithError('Couldn\'t find patchset %i in issue %i' %
2285 (parsed_issue_arg.patchset, self.GetIssue()))
2286
2287 fetch_info = revision_info['fetch']['http']
2288 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2289 RunGit(['cherry-pick', 'FETCH_HEAD'])
2290 self.SetIssue(self.GetIssue())
2291 self.SetPatchset(patchset)
2292 print('Committed patch for issue %i pathset %i locally' %
2293 (self.GetIssue(), self.GetPatchset()))
2294 return 0
2295
2296 @staticmethod
2297 def ParseIssueURL(parsed_url):
2298 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2299 return None
2300 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2301 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2302 # Short urls like https://domain/<issue_number> can be used, but don't allow
2303 # specifying the patchset (you'd 404), but we allow that here.
2304 if parsed_url.path == '/':
2305 part = parsed_url.fragment
2306 else:
2307 part = parsed_url.path
2308 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2309 if match:
2310 return _ParsedIssueNumberArgument(
2311 issue=int(match.group(2)),
2312 patchset=int(match.group(4)) if match.group(4) else None,
2313 hostname=parsed_url.netloc)
2314 return None
2315
tandrii16e0b4e2016-06-07 10:34:28 -07002316 def _GerritCommitMsgHookCheck(self, offer_removal):
2317 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2318 if not os.path.exists(hook):
2319 return
2320 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2321 # custom developer made one.
2322 data = gclient_utils.FileRead(hook)
2323 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2324 return
2325 print('Warning: you have Gerrit commit-msg hook installed.\n'
2326 'It is not neccessary for uploading with git cl in squash mode, '
2327 'and may interfere with it in subtle ways.\n'
2328 'We recommend you remove the commit-msg hook.')
2329 if offer_removal:
2330 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2331 if reply.lower().startswith('y'):
2332 gclient_utils.rm_file_or_tree(hook)
2333 print('Gerrit commit-msg hook removed.')
2334 else:
2335 print('OK, will keep Gerrit commit-msg hook in place.')
2336
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002337 def CMDUploadChange(self, options, args, change):
2338 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002339 if options.squash and options.no_squash:
2340 DieWithError('Can only use one of --squash or --no-squash')
tandrii26f3e4e2016-06-10 08:37:04 -07002341 # TODO(tandrii): remove this by June 20.
2342 if (RunGit(['config', '--bool', 'gerrit.squash-uploads'],
2343 error_ok=True).strip() != 'false' and not options.squash and
2344 not options.no_squash):
2345 print('\n\nHi! You are using git cl upload in --no-squash mode.\n'
2346 'Chrome infrastructure wants to make --squash the default.\n'
2347 'To ensure that --no-squash is still the default for YOU do:\n'
2348 ' git config --bool gerrit.squash-uploads false\n'
2349 'See https://goo.gl/dnK2gV (use chromium.org account!) and '
2350 'let us know what you think. Thanks!\n'
2351 'BUG: http://crbug.com/611892\n\n')
2352
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002353 options.squash = ((settings.GetSquashGerritUploads() or options.squash) and
2354 not options.no_squash)
tandrii26f3e4e2016-06-10 08:37:04 -07002355
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002356 # We assume the remote called "origin" is the one we want.
2357 # It is probably not worthwhile to support different workflows.
2358 gerrit_remote = 'origin'
2359
2360 remote, remote_branch = self.GetRemoteBranch()
2361 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2362 pending_prefix='')
2363
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002364 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002365 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002366 if not self.GetIssue():
2367 # TODO(tandrii): deperecate this after 2016Q2. Backwards compatibility
2368 # with shadow branch, which used to contain change-id for a given
2369 # branch, using which we can fetch actual issue number and set it as the
2370 # property of the branch, which is the new way.
2371 message = RunGitSilent([
2372 'show', '--format=%B', '-s',
2373 'refs/heads/git_cl_uploads/%s' % self.GetBranch()])
2374 if message:
2375 change_ids = git_footers.get_footer_change_id(message.strip())
2376 if change_ids and len(change_ids) == 1:
2377 details = self._GetChangeDetail(issue=change_ids[0])
2378 if details:
2379 print('WARNING: found old upload in branch git_cl_uploads/%s '
2380 'corresponding to issue %s' %
2381 (self.GetBranch(), details['_number']))
2382 self.SetIssue(details['_number'])
2383 if not self.GetIssue():
2384 DieWithError(
2385 '\n' # For readability of the blob below.
2386 'Found old upload in branch git_cl_uploads/%s, '
2387 'but failed to find corresponding Gerrit issue.\n'
2388 'If you know the issue number, set it manually first:\n'
2389 ' git cl issue 123456\n'
2390 'If you intended to upload this CL as new issue, '
2391 'just delete or rename the old upload branch:\n'
2392 ' git rename-branch git_cl_uploads/%s old_upload-%s\n'
2393 'After that, please run git cl upload again.' %
2394 tuple([self.GetBranch()] * 3))
2395 # End of backwards compatability.
2396
2397 if self.GetIssue():
2398 # Try to get the message from a previous upload.
2399 message = self.GetDescription()
2400 if not message:
2401 DieWithError(
2402 'failed to fetch description from current Gerrit issue %d\n'
2403 '%s' % (self.GetIssue(), self.GetIssueURL()))
2404 change_id = self._GetChangeDetail()['change_id']
2405 while True:
2406 footer_change_ids = git_footers.get_footer_change_id(message)
2407 if footer_change_ids == [change_id]:
2408 break
2409 if not footer_change_ids:
2410 message = git_footers.add_footer_change_id(message, change_id)
2411 print('WARNING: appended missing Change-Id to issue description')
2412 continue
2413 # There is already a valid footer but with different or several ids.
2414 # Doing this automatically is non-trivial as we don't want to lose
2415 # existing other footers, yet we want to append just 1 desired
2416 # Change-Id. Thus, just create a new footer, but let user verify the
2417 # new description.
2418 message = '%s\n\nChange-Id: %s' % (message, change_id)
2419 print(
2420 'WARNING: issue %s has Change-Id footer(s):\n'
2421 ' %s\n'
2422 'but issue has Change-Id %s, according to Gerrit.\n'
2423 'Please, check the proposed correction to the description, '
2424 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2425 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2426 change_id))
2427 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2428 if not options.force:
2429 change_desc = ChangeDescription(message)
2430 change_desc.prompt()
2431 message = change_desc.description
2432 if not message:
2433 DieWithError("Description is empty. Aborting...")
2434 # Continue the while loop.
2435 # Sanity check of this code - we should end up with proper message
2436 # footer.
2437 assert [change_id] == git_footers.get_footer_change_id(message)
2438 change_desc = ChangeDescription(message)
2439 else:
2440 change_desc = ChangeDescription(
2441 options.message or CreateDescriptionFromLog(args))
2442 if not options.force:
2443 change_desc.prompt()
2444 if not change_desc.description:
2445 DieWithError("Description is empty. Aborting...")
2446 message = change_desc.description
2447 change_ids = git_footers.get_footer_change_id(message)
2448 if len(change_ids) > 1:
2449 DieWithError('too many Change-Id footers, at most 1 allowed.')
2450 if not change_ids:
2451 # Generate the Change-Id automatically.
2452 message = git_footers.add_footer_change_id(
2453 message, GenerateGerritChangeId(message))
2454 change_desc.set_description(message)
2455 change_ids = git_footers.get_footer_change_id(message)
2456 assert len(change_ids) == 1
2457 change_id = change_ids[0]
2458
2459 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2460 if remote is '.':
2461 # If our upstream branch is local, we base our squashed commit on its
2462 # squashed version.
2463 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2464 # Check the squashed hash of the parent.
2465 parent = RunGit(['config',
2466 'branch.%s.gerritsquashhash' % upstream_branch_name],
2467 error_ok=True).strip()
2468 # Verify that the upstream branch has been uploaded too, otherwise
2469 # Gerrit will create additional CLs when uploading.
2470 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2471 RunGitSilent(['rev-parse', parent + ':'])):
2472 # TODO(tandrii): remove "old depot_tools" part on April 12, 2016.
2473 DieWithError(
2474 'Upload upstream branch %s first.\n'
2475 'Note: maybe you\'ve uploaded it with --no-squash or with an old '
2476 'version of depot_tools. If so, then re-upload it with:\n'
2477 ' git cl upload --squash\n' % upstream_branch_name)
2478 else:
2479 parent = self.GetCommonAncestorWithUpstream()
2480
2481 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2482 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2483 '-m', message]).strip()
2484 else:
2485 change_desc = ChangeDescription(
2486 options.message or CreateDescriptionFromLog(args))
2487 if not change_desc.description:
2488 DieWithError("Description is empty. Aborting...")
2489
2490 if not git_footers.get_footer_change_id(change_desc.description):
2491 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002492 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2493 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002494 ref_to_push = 'HEAD'
2495 parent = '%s/%s' % (gerrit_remote, branch)
2496 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2497
2498 assert change_desc
2499 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2500 ref_to_push)]).splitlines()
2501 if len(commits) > 1:
2502 print('WARNING: This will upload %d commits. Run the following command '
2503 'to see which commits will be uploaded: ' % len(commits))
2504 print('git log %s..%s' % (parent, ref_to_push))
2505 print('You can also use `git squash-branch` to squash these into a '
2506 'single commit.')
2507 ask_for_data('About to upload; enter to confirm.')
2508
2509 if options.reviewers or options.tbr_owners:
2510 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2511 change)
2512
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002513 # Extra options that can be specified at push time. Doc:
2514 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2515 refspec_opts = []
2516 if options.title:
2517 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2518 # reverse on its side.
2519 if '_' in options.title:
2520 print('WARNING: underscores in title will be converted to spaces.')
2521 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2522
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002523 if options.send_mail:
2524 if not change_desc.get_reviewers():
2525 DieWithError('Must specify reviewers to send email.')
2526 refspec_opts.append('notify=ALL')
2527 else:
2528 refspec_opts.append('notify=NONE')
2529
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002530 cc = self.GetCCList().split(',')
2531 if options.cc:
2532 cc.extend(options.cc)
2533 cc = filter(None, cc)
2534 if cc:
tandrii@chromium.org074c2af2016-06-03 23:18:40 +00002535 refspec_opts.extend('cc=' + email.strip() for email in cc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002536
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002537 if change_desc.get_reviewers():
2538 refspec_opts.extend('r=' + email.strip()
2539 for email in change_desc.get_reviewers())
2540
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002541 refspec_suffix = ''
2542 if refspec_opts:
2543 refspec_suffix = '%' + ','.join(refspec_opts)
2544 assert ' ' not in refspec_suffix, (
2545 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002546 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002547
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002548 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002549 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002550 print_stdout=True,
2551 # Flush after every line: useful for seeing progress when running as
2552 # recipe.
2553 filter_fn=lambda _: sys.stdout.flush())
2554
2555 if options.squash:
2556 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2557 change_numbers = [m.group(1)
2558 for m in map(regex.match, push_stdout.splitlines())
2559 if m]
2560 if len(change_numbers) != 1:
2561 DieWithError(
2562 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2563 'Change-Id: %s') % (len(change_numbers), change_id))
2564 self.SetIssue(change_numbers[0])
2565 RunGit(['config', 'branch.%s.gerritsquashhash' % self.GetBranch(),
2566 ref_to_push])
2567 return 0
2568
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002569 def _AddChangeIdToCommitMessage(self, options, args):
2570 """Re-commits using the current message, assumes the commit hook is in
2571 place.
2572 """
2573 log_desc = options.message or CreateDescriptionFromLog(args)
2574 git_command = ['commit', '--amend', '-m', log_desc]
2575 RunGit(git_command)
2576 new_log_desc = CreateDescriptionFromLog(args)
2577 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002578 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002579 return new_log_desc
2580 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002581 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002582
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002583 def SetCQState(self, new_state):
2584 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
2585 # TODO(tandrii): maybe allow configurability in codereview.settings or by
2586 # self-discovery of label config for this CL using REST API.
2587 vote_map = {
2588 _CQState.NONE: 0,
2589 _CQState.DRY_RUN: 1,
2590 _CQState.COMMIT : 2,
2591 }
2592 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2593 labels={'Commit-Queue': vote_map[new_state]})
2594
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002595
2596_CODEREVIEW_IMPLEMENTATIONS = {
2597 'rietveld': _RietveldChangelistImpl,
2598 'gerrit': _GerritChangelistImpl,
2599}
2600
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002601
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002602def _add_codereview_select_options(parser):
2603 """Appends --gerrit and --rietveld options to force specific codereview."""
2604 parser.codereview_group = optparse.OptionGroup(
2605 parser, 'EXPERIMENTAL! Codereview override options')
2606 parser.add_option_group(parser.codereview_group)
2607 parser.codereview_group.add_option(
2608 '--gerrit', action='store_true',
2609 help='Force the use of Gerrit for codereview')
2610 parser.codereview_group.add_option(
2611 '--rietveld', action='store_true',
2612 help='Force the use of Rietveld for codereview')
2613
2614
2615def _process_codereview_select_options(parser, options):
2616 if options.gerrit and options.rietveld:
2617 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2618 options.forced_codereview = None
2619 if options.gerrit:
2620 options.forced_codereview = 'gerrit'
2621 elif options.rietveld:
2622 options.forced_codereview = 'rietveld'
2623
2624
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002625class ChangeDescription(object):
2626 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002627 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002628 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002629
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002630 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002631 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002632
agable@chromium.org42c20792013-09-12 17:34:49 +00002633 @property # www.logilab.org/ticket/89786
2634 def description(self): # pylint: disable=E0202
2635 return '\n'.join(self._description_lines)
2636
2637 def set_description(self, desc):
2638 if isinstance(desc, basestring):
2639 lines = desc.splitlines()
2640 else:
2641 lines = [line.rstrip() for line in desc]
2642 while lines and not lines[0]:
2643 lines.pop(0)
2644 while lines and not lines[-1]:
2645 lines.pop(-1)
2646 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002647
piman@chromium.org336f9122014-09-04 02:16:55 +00002648 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002649 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002650 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002651 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002652 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002653 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002654
agable@chromium.org42c20792013-09-12 17:34:49 +00002655 # Get the set of R= and TBR= lines and remove them from the desciption.
2656 regexp = re.compile(self.R_LINE)
2657 matches = [regexp.match(line) for line in self._description_lines]
2658 new_desc = [l for i, l in enumerate(self._description_lines)
2659 if not matches[i]]
2660 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002661
agable@chromium.org42c20792013-09-12 17:34:49 +00002662 # Construct new unified R= and TBR= lines.
2663 r_names = []
2664 tbr_names = []
2665 for match in matches:
2666 if not match:
2667 continue
2668 people = cleanup_list([match.group(2).strip()])
2669 if match.group(1) == 'TBR':
2670 tbr_names.extend(people)
2671 else:
2672 r_names.extend(people)
2673 for name in r_names:
2674 if name not in reviewers:
2675 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002676 if add_owners_tbr:
2677 owners_db = owners.Database(change.RepositoryRoot(),
2678 fopen=file, os_path=os.path, glob=glob.glob)
2679 all_reviewers = set(tbr_names + reviewers)
2680 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2681 all_reviewers)
2682 tbr_names.extend(owners_db.reviewers_for(missing_files,
2683 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002684 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2685 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2686
2687 # Put the new lines in the description where the old first R= line was.
2688 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2689 if 0 <= line_loc < len(self._description_lines):
2690 if new_tbr_line:
2691 self._description_lines.insert(line_loc, new_tbr_line)
2692 if new_r_line:
2693 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002694 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002695 if new_r_line:
2696 self.append_footer(new_r_line)
2697 if new_tbr_line:
2698 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002699
2700 def prompt(self):
2701 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002702 self.set_description([
2703 '# Enter a description of the change.',
2704 '# This will be displayed on the codereview site.',
2705 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002706 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002707 '--------------------',
2708 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002709
agable@chromium.org42c20792013-09-12 17:34:49 +00002710 regexp = re.compile(self.BUG_LINE)
2711 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00002712 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00002713 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002714 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002715 if not content:
2716 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002717 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002718
2719 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002720 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2721 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002722 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002723 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002724
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002725 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00002726 """Adds a footer line to the description.
2727
2728 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
2729 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
2730 that Gerrit footers are always at the end.
2731 """
2732 parsed_footer_line = git_footers.parse_footer(line)
2733 if parsed_footer_line:
2734 # Line is a gerrit footer in the form: Footer-Key: any value.
2735 # Thus, must be appended observing Gerrit footer rules.
2736 self.set_description(
2737 git_footers.add_footer(self.description,
2738 key=parsed_footer_line[0],
2739 value=parsed_footer_line[1]))
2740 return
2741
2742 if not self._description_lines:
2743 self._description_lines.append(line)
2744 return
2745
2746 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
2747 if gerrit_footers:
2748 # git_footers.split_footers ensures that there is an empty line before
2749 # actual (gerrit) footers, if any. We have to keep it that way.
2750 assert top_lines and top_lines[-1] == ''
2751 top_lines, separator = top_lines[:-1], top_lines[-1:]
2752 else:
2753 separator = [] # No need for separator if there are no gerrit_footers.
2754
2755 prev_line = top_lines[-1] if top_lines else ''
2756 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
2757 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2758 top_lines.append('')
2759 top_lines.append(line)
2760 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002761
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002762 def get_reviewers(self):
2763 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002764 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
2765 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002766 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002767
2768
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002769def get_approving_reviewers(props):
2770 """Retrieves the reviewers that approved a CL from the issue properties with
2771 messages.
2772
2773 Note that the list may contain reviewers that are not committer, thus are not
2774 considered by the CQ.
2775 """
2776 return sorted(
2777 set(
2778 message['sender']
2779 for message in props['messages']
2780 if message['approval'] and message['sender'] in props['reviewers']
2781 )
2782 )
2783
2784
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002785def FindCodereviewSettingsFile(filename='codereview.settings'):
2786 """Finds the given file starting in the cwd and going up.
2787
2788 Only looks up to the top of the repository unless an
2789 'inherit-review-settings-ok' file exists in the root of the repository.
2790 """
2791 inherit_ok_file = 'inherit-review-settings-ok'
2792 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002793 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002794 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2795 root = '/'
2796 while True:
2797 if filename in os.listdir(cwd):
2798 if os.path.isfile(os.path.join(cwd, filename)):
2799 return open(os.path.join(cwd, filename))
2800 if cwd == root:
2801 break
2802 cwd = os.path.dirname(cwd)
2803
2804
2805def LoadCodereviewSettingsFromFile(fileobj):
2806 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002807 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002808
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002809 def SetProperty(name, setting, unset_error_ok=False):
2810 fullname = 'rietveld.' + name
2811 if setting in keyvals:
2812 RunGit(['config', fullname, keyvals[setting]])
2813 else:
2814 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2815
2816 SetProperty('server', 'CODE_REVIEW_SERVER')
2817 # Only server setting is required. Other settings can be absent.
2818 # In that case, we ignore errors raised during option deletion attempt.
2819 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002820 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002821 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2822 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002823 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002824 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002825 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2826 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002827 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002828 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002829 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002830 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2831 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002832
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002833 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002834 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002835
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002836 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii768ec9a2016-06-16 07:39:10 -07002837 cur = RunGit(['config', '--bool', 'gerrit.squash-uploads'], error_ok=True)
2838 if not cur: # Set the value only if it wasn't set by the user manually.
2839 RunGit(['config', 'gerrit.squash-uploads',
2840 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002841
tandrii@chromium.org28253532016-04-14 13:46:56 +00002842 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00002843 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00002844 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
2845
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002846 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2847 #should be of the form
2848 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2849 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2850 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2851 keyvals['ORIGIN_URL_CONFIG']])
2852
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002853
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002854def urlretrieve(source, destination):
2855 """urllib is broken for SSL connections via a proxy therefore we
2856 can't use urllib.urlretrieve()."""
2857 with open(destination, 'w') as f:
2858 f.write(urllib2.urlopen(source).read())
2859
2860
ukai@chromium.org712d6102013-11-27 00:52:58 +00002861def hasSheBang(fname):
2862 """Checks fname is a #! script."""
2863 with open(fname) as f:
2864 return f.read(2).startswith('#!')
2865
2866
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00002867# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
2868def DownloadHooks(*args, **kwargs):
2869 pass
2870
2871
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002872def DownloadGerritHook(force):
2873 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002874
2875 Args:
2876 force: True to update hooks. False to install hooks if not present.
2877 """
2878 if not settings.GetIsGerrit():
2879 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00002880 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002881 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2882 if not os.access(dst, os.X_OK):
2883 if os.path.exists(dst):
2884 if not force:
2885 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002886 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002887 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002888 if not hasSheBang(dst):
2889 DieWithError('Not a script: %s\n'
2890 'You need to download from\n%s\n'
2891 'into .git/hooks/commit-msg and '
2892 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002893 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2894 except Exception:
2895 if os.path.exists(dst):
2896 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002897 DieWithError('\nFailed to download hooks.\n'
2898 'You need to download from\n%s\n'
2899 'into .git/hooks/commit-msg and '
2900 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002901
2902
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002903
2904def GetRietveldCodereviewSettingsInteractively():
2905 """Prompt the user for settings."""
2906 server = settings.GetDefaultServerUrl(error_ok=True)
2907 prompt = 'Rietveld server (host[:port])'
2908 prompt += ' [%s]' % (server or DEFAULT_SERVER)
2909 newserver = ask_for_data(prompt + ':')
2910 if not server and not newserver:
2911 newserver = DEFAULT_SERVER
2912 if newserver:
2913 newserver = gclient_utils.UpgradeToHttps(newserver)
2914 if newserver != server:
2915 RunGit(['config', 'rietveld.server', newserver])
2916
2917 def SetProperty(initial, caption, name, is_url):
2918 prompt = caption
2919 if initial:
2920 prompt += ' ("x" to clear) [%s]' % initial
2921 new_val = ask_for_data(prompt + ':')
2922 if new_val == 'x':
2923 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
2924 elif new_val:
2925 if is_url:
2926 new_val = gclient_utils.UpgradeToHttps(new_val)
2927 if new_val != initial:
2928 RunGit(['config', 'rietveld.' + name, new_val])
2929
2930 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
2931 SetProperty(settings.GetDefaultPrivateFlag(),
2932 'Private flag (rietveld only)', 'private', False)
2933 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
2934 'tree-status-url', False)
2935 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
2936 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
2937 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
2938 'run-post-upload-hook', False)
2939
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002940@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002941def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002942 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002943
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002944 print('WARNING: git cl config works for Rietveld only.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002945 'For Gerrit, see http://crbug.com/603116.')
2946 # TODO(tandrii): add Gerrit support as part of http://crbug.com/603116.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00002947 parser.add_option('--activate-update', action='store_true',
2948 help='activate auto-updating [rietveld] section in '
2949 '.git/config')
2950 parser.add_option('--deactivate-update', action='store_true',
2951 help='deactivate auto-updating [rietveld] section in '
2952 '.git/config')
2953 options, args = parser.parse_args(args)
2954
2955 if options.deactivate_update:
2956 RunGit(['config', 'rietveld.autoupdate', 'false'])
2957 return
2958
2959 if options.activate_update:
2960 RunGit(['config', '--unset', 'rietveld.autoupdate'])
2961 return
2962
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002963 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002964 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002965 return 0
2966
2967 url = args[0]
2968 if not url.endswith('codereview.settings'):
2969 url = os.path.join(url, 'codereview.settings')
2970
2971 # Load code review settings and download hooks (if available).
2972 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
2973 return 0
2974
2975
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002976def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002977 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002978 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
2979 branch = ShortBranchName(branchref)
2980 _, args = parser.parse_args(args)
2981 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07002982 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002983 return RunGit(['config', 'branch.%s.base-url' % branch],
2984 error_ok=False).strip()
2985 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002986 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002987 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
2988 error_ok=False).strip()
2989
2990
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002991def color_for_status(status):
2992 """Maps a Changelist status to color, for CMDstatus and other tools."""
2993 return {
2994 'unsent': Fore.RED,
2995 'waiting': Fore.BLUE,
2996 'reply': Fore.YELLOW,
2997 'lgtm': Fore.GREEN,
2998 'commit': Fore.MAGENTA,
2999 'closed': Fore.CYAN,
3000 'error': Fore.WHITE,
3001 }.get(status, Fore.WHITE)
3002
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003003
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003004def get_cl_statuses(changes, fine_grained, max_processes=None):
3005 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003006
3007 If fine_grained is true, this will fetch CL statuses from the server.
3008 Otherwise, simply indicate if there's a matching url for the given branches.
3009
3010 If max_processes is specified, it is used as the maximum number of processes
3011 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3012 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003013
3014 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003015 """
3016 # Silence upload.py otherwise it becomes unwieldly.
3017 upload.verbosity = 0
3018
3019 if fine_grained:
3020 # Process one branch synchronously to work through authentication, then
3021 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003022 if changes:
3023 fetch = lambda cl: (cl, cl.GetStatus())
3024 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003025
kmarshall3bff56b2016-06-06 18:31:47 -07003026 if not changes:
3027 # Exit early if there was only one branch to fetch.
3028 return
3029
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003030 changes_to_fetch = changes[1:]
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003031 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003032 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003033 if max_processes is not None
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003034 else len(changes_to_fetch))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003035
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003036 fetched_cls = set()
3037 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003038 while True:
3039 try:
3040 row = it.next(timeout=5)
3041 except multiprocessing.TimeoutError:
3042 break
3043
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003044 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003045 yield row
3046
3047 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003048 for cl in set(changes_to_fetch) - fetched_cls:
3049 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003050
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003051 else:
3052 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003053 for cl in changes:
3054 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003055
rmistry@google.com2dd99862015-06-22 12:22:18 +00003056
3057def upload_branch_deps(cl, args):
3058 """Uploads CLs of local branches that are dependents of the current branch.
3059
3060 If the local branch dependency tree looks like:
3061 test1 -> test2.1 -> test3.1
3062 -> test3.2
3063 -> test2.2 -> test3.3
3064
3065 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3066 run on the dependent branches in this order:
3067 test2.1, test3.1, test3.2, test2.2, test3.3
3068
3069 Note: This function does not rebase your local dependent branches. Use it when
3070 you make a change to the parent branch that will not conflict with its
3071 dependent branches, and you would like their dependencies updated in
3072 Rietveld.
3073 """
3074 if git_common.is_dirty_git_tree('upload-branch-deps'):
3075 return 1
3076
3077 root_branch = cl.GetBranch()
3078 if root_branch is None:
3079 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3080 'Get on a branch!')
3081 if not cl.GetIssue() or not cl.GetPatchset():
3082 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3083 'patchset dependencies without an uploaded CL.')
3084
3085 branches = RunGit(['for-each-ref',
3086 '--format=%(refname:short) %(upstream:short)',
3087 'refs/heads'])
3088 if not branches:
3089 print('No local branches found.')
3090 return 0
3091
3092 # Create a dictionary of all local branches to the branches that are dependent
3093 # on it.
3094 tracked_to_dependents = collections.defaultdict(list)
3095 for b in branches.splitlines():
3096 tokens = b.split()
3097 if len(tokens) == 2:
3098 branch_name, tracked = tokens
3099 tracked_to_dependents[tracked].append(branch_name)
3100
vapiera7fbd5a2016-06-16 09:17:49 -07003101 print()
3102 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003103 dependents = []
3104 def traverse_dependents_preorder(branch, padding=''):
3105 dependents_to_process = tracked_to_dependents.get(branch, [])
3106 padding += ' '
3107 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003108 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003109 dependents.append(dependent)
3110 traverse_dependents_preorder(dependent, padding)
3111 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003112 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003113
3114 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003115 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003116 return 0
3117
vapiera7fbd5a2016-06-16 09:17:49 -07003118 print('This command will checkout all dependent branches and run '
3119 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003120 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3121
andybons@chromium.org962f9462016-02-03 20:00:42 +00003122 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003123 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003124 args.extend(['-t', 'Updated patchset dependency'])
3125
rmistry@google.com2dd99862015-06-22 12:22:18 +00003126 # Record all dependents that failed to upload.
3127 failures = {}
3128 # Go through all dependents, checkout the branch and upload.
3129 try:
3130 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003131 print()
3132 print('--------------------------------------')
3133 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003134 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003135 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003136 try:
3137 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003138 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003139 failures[dependent_branch] = 1
3140 except: # pylint: disable=W0702
3141 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003142 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003143 finally:
3144 # Swap back to the original root branch.
3145 RunGit(['checkout', '-q', root_branch])
3146
vapiera7fbd5a2016-06-16 09:17:49 -07003147 print()
3148 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003149 for dependent_branch in dependents:
3150 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003151 print(' %s : %s' % (dependent_branch, upload_status))
3152 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003153
3154 return 0
3155
3156
kmarshall3bff56b2016-06-06 18:31:47 -07003157def CMDarchive(parser, args):
3158 """Archives and deletes branches associated with closed changelists."""
3159 parser.add_option(
3160 '-j', '--maxjobs', action='store', type=int,
3161 help='The maximum number of jobs to use when retrieving review status')
3162 parser.add_option(
3163 '-f', '--force', action='store_true',
3164 help='Bypasses the confirmation prompt.')
3165
3166 auth.add_auth_options(parser)
3167 options, args = parser.parse_args(args)
3168 if args:
3169 parser.error('Unsupported args: %s' % ' '.join(args))
3170 auth_config = auth.extract_auth_config_from_options(options)
3171
3172 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3173 if not branches:
3174 return 0
3175
vapiera7fbd5a2016-06-16 09:17:49 -07003176 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003177 changes = [Changelist(branchref=b, auth_config=auth_config)
3178 for b in branches.splitlines()]
3179 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3180 statuses = get_cl_statuses(changes,
3181 fine_grained=True,
3182 max_processes=options.maxjobs)
3183 proposal = [(cl.GetBranch(),
3184 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3185 for cl, status in statuses
3186 if status == 'closed']
3187 proposal.sort()
3188
3189 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003190 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003191 return 0
3192
3193 current_branch = GetCurrentBranch()
3194
vapiera7fbd5a2016-06-16 09:17:49 -07003195 print('\nBranches with closed issues that will be archived:\n')
3196 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
kmarshall3bff56b2016-06-06 18:31:47 -07003197 for next_item in proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003198 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003199
3200 if any(branch == current_branch for branch, _ in proposal):
3201 print('You are currently on a branch \'%s\' which is associated with a '
3202 'closed codereview issue, so archive cannot proceed. Please '
3203 'checkout another branch and run this command again.' %
3204 current_branch)
3205 return 1
3206
3207 if not options.force:
3208 if ask_for_data('\nProceed with deletion (Y/N)? ').lower() != 'y':
vapiera7fbd5a2016-06-16 09:17:49 -07003209 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003210 return 1
3211
3212 for branch, tagname in proposal:
3213 RunGit(['tag', tagname, branch])
3214 RunGit(['branch', '-D', branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003215 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003216
3217 return 0
3218
3219
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003220def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003221 """Show status of changelists.
3222
3223 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003224 - Red not sent for review or broken
3225 - Blue waiting for review
3226 - Yellow waiting for you to reply to review
3227 - Green LGTM'ed
3228 - Magenta in the commit queue
3229 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003230
3231 Also see 'git cl comments'.
3232 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003233 parser.add_option('--field',
3234 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003235 parser.add_option('-f', '--fast', action='store_true',
3236 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003237 parser.add_option(
3238 '-j', '--maxjobs', action='store', type=int,
3239 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003240
3241 auth.add_auth_options(parser)
3242 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003243 if args:
3244 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003245 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003246
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003247 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003248 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003249 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003250 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003251 elif options.field == 'id':
3252 issueid = cl.GetIssue()
3253 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003254 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003255 elif options.field == 'patch':
3256 patchset = cl.GetPatchset()
3257 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003258 print(patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003259 elif options.field == 'url':
3260 url = cl.GetIssueURL()
3261 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003262 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003263 return 0
3264
3265 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3266 if not branches:
3267 print('No local branch found.')
3268 return 0
3269
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003270 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003271 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003272 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003273 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003274 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003275 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003276 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003277
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003278 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003279 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3280 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3281 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003282 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003283 c, status = output.next()
3284 branch_statuses[c.GetBranch()] = status
3285 status = branch_statuses.pop(branch)
3286 url = cl.GetIssueURL()
3287 if url and (not status or status == 'error'):
3288 # The issue probably doesn't exist anymore.
3289 url += ' (broken)'
3290
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003291 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003292 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003293 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003294 color = ''
3295 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003296 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003297 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003298 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003299 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003300
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003301 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003302 print()
3303 print('Current branch:',)
3304 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003305 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003306 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003307 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003308 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003309 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003310 print('Issue description:')
3311 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003312 return 0
3313
3314
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003315def colorize_CMDstatus_doc():
3316 """To be called once in main() to add colors to git cl status help."""
3317 colors = [i for i in dir(Fore) if i[0].isupper()]
3318
3319 def colorize_line(line):
3320 for color in colors:
3321 if color in line.upper():
3322 # Extract whitespaces first and the leading '-'.
3323 indent = len(line) - len(line.lstrip(' ')) + 1
3324 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3325 return line
3326
3327 lines = CMDstatus.__doc__.splitlines()
3328 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3329
3330
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003331@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003332def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003333 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003334
3335 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003336 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003337 parser.add_option('-r', '--reverse', action='store_true',
3338 help='Lookup the branch(es) for the specified issues. If '
3339 'no issues are specified, all branches with mapped '
3340 'issues will be listed.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003341 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003342 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003343 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003344
dnj@chromium.org406c4402015-03-03 17:22:28 +00003345 if options.reverse:
3346 branches = RunGit(['for-each-ref', 'refs/heads',
3347 '--format=%(refname:short)']).splitlines()
3348
3349 # Reverse issue lookup.
3350 issue_branch_map = {}
3351 for branch in branches:
3352 cl = Changelist(branchref=branch)
3353 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3354 if not args:
3355 args = sorted(issue_branch_map.iterkeys())
3356 for issue in args:
3357 if not issue:
3358 continue
vapiera7fbd5a2016-06-16 09:17:49 -07003359 print('Branch for issue number %s: %s' % (
3360 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
dnj@chromium.org406c4402015-03-03 17:22:28 +00003361 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003362 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003363 if len(args) > 0:
3364 try:
3365 issue = int(args[0])
3366 except ValueError:
3367 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003368 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003369 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003370 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003371 return 0
3372
3373
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003374def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003375 """Shows or posts review comments for any changelist."""
3376 parser.add_option('-a', '--add-comment', dest='comment',
3377 help='comment to add to an issue')
3378 parser.add_option('-i', dest='issue',
3379 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003380 parser.add_option('-j', '--json-file',
3381 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003382 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003383 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003384 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003385
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003386 issue = None
3387 if options.issue:
3388 try:
3389 issue = int(options.issue)
3390 except ValueError:
3391 DieWithError('A review issue id is expected to be a number')
3392
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003393 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003394
3395 if options.comment:
3396 cl.AddComment(options.comment)
3397 return 0
3398
3399 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003400 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003401 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003402 summary.append({
3403 'date': message['date'],
3404 'lgtm': False,
3405 'message': message['text'],
3406 'not_lgtm': False,
3407 'sender': message['sender'],
3408 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003409 if message['disapproval']:
3410 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003411 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003412 elif message['approval']:
3413 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003414 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003415 elif message['sender'] == data['owner_email']:
3416 color = Fore.MAGENTA
3417 else:
3418 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003419 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003420 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003421 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003422 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003423 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003424 if options.json_file:
3425 with open(options.json_file, 'wb') as f:
3426 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003427 return 0
3428
3429
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003430@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003431def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003432 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003433 parser.add_option('-d', '--display', action='store_true',
3434 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003435 parser.add_option('-n', '--new-description',
3436 help='New description to set for this issue (- for stdin)')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003437
3438 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003439 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003440 options, args = parser.parse_args(args)
3441 _process_codereview_select_options(parser, options)
3442
3443 target_issue = None
3444 if len(args) > 0:
3445 issue_arg = ParseIssueNumberArgument(args[0])
3446 if not issue_arg.valid:
3447 parser.print_help()
3448 return 1
3449 target_issue = issue_arg.issue
3450
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003451 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003452
3453 cl = Changelist(
3454 auth_config=auth_config, issue=target_issue,
3455 codereview=options.forced_codereview)
3456
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003457 if not cl.GetIssue():
3458 DieWithError('This branch has no associated changelist.')
3459 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003460
smut@google.com34fb6b12015-07-13 20:03:26 +00003461 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003462 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003463 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003464
3465 if options.new_description:
3466 text = options.new_description
3467 if text == '-':
3468 text = '\n'.join(l.rstrip() for l in sys.stdin)
3469
3470 description.set_description(text)
3471 else:
3472 description.prompt()
3473
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003474 if cl.GetDescription() != description.description:
3475 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003476 return 0
3477
3478
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003479def CreateDescriptionFromLog(args):
3480 """Pulls out the commit log to use as a base for the CL description."""
3481 log_args = []
3482 if len(args) == 1 and not args[0].endswith('.'):
3483 log_args = [args[0] + '..']
3484 elif len(args) == 1 and args[0].endswith('...'):
3485 log_args = [args[0][:-1]]
3486 elif len(args) == 2:
3487 log_args = [args[0] + '..' + args[1]]
3488 else:
3489 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003490 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003491
3492
thestig@chromium.org44202a22014-03-11 19:22:18 +00003493def CMDlint(parser, args):
3494 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003495 parser.add_option('--filter', action='append', metavar='-x,+y',
3496 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003497 auth.add_auth_options(parser)
3498 options, args = parser.parse_args(args)
3499 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003500
3501 # Access to a protected member _XX of a client class
3502 # pylint: disable=W0212
3503 try:
3504 import cpplint
3505 import cpplint_chromium
3506 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003507 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003508 return 1
3509
3510 # Change the current working directory before calling lint so that it
3511 # shows the correct base.
3512 previous_cwd = os.getcwd()
3513 os.chdir(settings.GetRoot())
3514 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003515 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003516 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3517 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003518 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003519 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003520 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003521
3522 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003523 command = args + files
3524 if options.filter:
3525 command = ['--filter=' + ','.join(options.filter)] + command
3526 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003527
3528 white_regex = re.compile(settings.GetLintRegex())
3529 black_regex = re.compile(settings.GetLintIgnoreRegex())
3530 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3531 for filename in filenames:
3532 if white_regex.match(filename):
3533 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07003534 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003535 else:
3536 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3537 extra_check_functions)
3538 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003539 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003540 finally:
3541 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07003542 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003543 if cpplint._cpplint_state.error_count != 0:
3544 return 1
3545 return 0
3546
3547
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003548def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003549 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003550 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003551 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003552 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003553 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003554 auth.add_auth_options(parser)
3555 options, args = parser.parse_args(args)
3556 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003557
sbc@chromium.org71437c02015-04-09 19:29:40 +00003558 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07003559 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003560 return 1
3561
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003562 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003563 if args:
3564 base_branch = args[0]
3565 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003566 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003567 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003568
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003569 cl.RunHook(
3570 committing=not options.upload,
3571 may_prompt=False,
3572 verbose=options.verbose,
3573 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003574 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003575
3576
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003577def GenerateGerritChangeId(message):
3578 """Returns Ixxxxxx...xxx change id.
3579
3580 Works the same way as
3581 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3582 but can be called on demand on all platforms.
3583
3584 The basic idea is to generate git hash of a state of the tree, original commit
3585 message, author/committer info and timestamps.
3586 """
3587 lines = []
3588 tree_hash = RunGitSilent(['write-tree'])
3589 lines.append('tree %s' % tree_hash.strip())
3590 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3591 if code == 0:
3592 lines.append('parent %s' % parent.strip())
3593 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3594 lines.append('author %s' % author.strip())
3595 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3596 lines.append('committer %s' % committer.strip())
3597 lines.append('')
3598 # Note: Gerrit's commit-hook actually cleans message of some lines and
3599 # whitespace. This code is not doing this, but it clearly won't decrease
3600 # entropy.
3601 lines.append(message)
3602 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3603 stdin='\n'.join(lines))
3604 return 'I%s' % change_hash.strip()
3605
3606
wittman@chromium.org455dc922015-01-26 20:15:50 +00003607def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3608 """Computes the remote branch ref to use for the CL.
3609
3610 Args:
3611 remote (str): The git remote for the CL.
3612 remote_branch (str): The git remote branch for the CL.
3613 target_branch (str): The target branch specified by the user.
3614 pending_prefix (str): The pending prefix from the settings.
3615 """
3616 if not (remote and remote_branch):
3617 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003618
wittman@chromium.org455dc922015-01-26 20:15:50 +00003619 if target_branch:
3620 # Cannonicalize branch references to the equivalent local full symbolic
3621 # refs, which are then translated into the remote full symbolic refs
3622 # below.
3623 if '/' not in target_branch:
3624 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3625 else:
3626 prefix_replacements = (
3627 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3628 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3629 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3630 )
3631 match = None
3632 for regex, replacement in prefix_replacements:
3633 match = re.search(regex, target_branch)
3634 if match:
3635 remote_branch = target_branch.replace(match.group(0), replacement)
3636 break
3637 if not match:
3638 # This is a branch path but not one we recognize; use as-is.
3639 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003640 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3641 # Handle the refs that need to land in different refs.
3642 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003643
wittman@chromium.org455dc922015-01-26 20:15:50 +00003644 # Create the true path to the remote branch.
3645 # Does the following translation:
3646 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3647 # * refs/remotes/origin/master -> refs/heads/master
3648 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3649 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3650 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3651 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3652 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3653 'refs/heads/')
3654 elif remote_branch.startswith('refs/remotes/branch-heads'):
3655 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3656 # If a pending prefix exists then replace refs/ with it.
3657 if pending_prefix:
3658 remote_branch = remote_branch.replace('refs/', pending_prefix)
3659 return remote_branch
3660
3661
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003662def cleanup_list(l):
3663 """Fixes a list so that comma separated items are put as individual items.
3664
3665 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3666 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3667 """
3668 items = sum((i.split(',') for i in l), [])
3669 stripped_items = (i.strip() for i in items)
3670 return sorted(filter(None, stripped_items))
3671
3672
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003673@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003674def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003675 """Uploads the current changelist to codereview.
3676
3677 Can skip dependency patchset uploads for a branch by running:
3678 git config branch.branch_name.skip-deps-uploads True
3679 To unset run:
3680 git config --unset branch.branch_name.skip-deps-uploads
3681 Can also set the above globally by using the --global flag.
3682 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003683 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3684 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003685 parser.add_option('--bypass-watchlists', action='store_true',
3686 dest='bypass_watchlists',
3687 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003688 parser.add_option('-f', action='store_true', dest='force',
3689 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003690 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003691 parser.add_option('-t', dest='title',
3692 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003693 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003694 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003695 help='reviewer email addresses')
3696 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003697 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003698 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003699 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003700 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003701 parser.add_option('--emulate_svn_auto_props',
3702 '--emulate-svn-auto-props',
3703 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003704 dest="emulate_svn_auto_props",
3705 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003706 parser.add_option('-c', '--use-commit-queue', action='store_true',
3707 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003708 parser.add_option('--private', action='store_true',
3709 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003710 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003711 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003712 metavar='TARGET',
3713 help='Apply CL to remote ref TARGET. ' +
3714 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003715 parser.add_option('--squash', action='store_true',
3716 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003717 parser.add_option('--no-squash', action='store_true',
3718 help='Don\'t squash multiple commits into one ' +
3719 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003720 parser.add_option('--email', default=None,
3721 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003722 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3723 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003724 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3725 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003726 help='Send the patchset to do a CQ dry run right after '
3727 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003728 parser.add_option('--dependencies', action='store_true',
3729 help='Uploads CLs of all the local branches that depend on '
3730 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003731
rmistry@google.com2dd99862015-06-22 12:22:18 +00003732 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003733 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003734 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003735 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003736 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003737 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003738 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003739
sbc@chromium.org71437c02015-04-09 19:29:40 +00003740 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003741 return 1
3742
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003743 options.reviewers = cleanup_list(options.reviewers)
3744 options.cc = cleanup_list(options.cc)
3745
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003746 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3747 settings.GetIsGerrit()
3748
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003749 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00003750 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003751
3752
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003753def IsSubmoduleMergeCommit(ref):
3754 # When submodules are added to the repo, we expect there to be a single
3755 # non-git-svn merge commit at remote HEAD with a signature comment.
3756 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003757 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003758 return RunGit(cmd) != ''
3759
3760
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003761def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003762 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003763
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003764 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3765 upstream and closes the issue automatically and atomically.
3766
3767 Otherwise (in case of Rietveld):
3768 Squashes branch into a single commit.
3769 Updates changelog with metadata (e.g. pointer to review).
3770 Pushes/dcommits the code upstream.
3771 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003772 """
3773 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3774 help='bypass upload presubmit hook')
3775 parser.add_option('-m', dest='message',
3776 help="override review description")
3777 parser.add_option('-f', action='store_true', dest='force',
3778 help="force yes to questions (don't prompt)")
3779 parser.add_option('-c', dest='contributor',
3780 help="external contributor for patch (appended to " +
3781 "description and used as author for git). Should be " +
3782 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003783 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003784 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003785 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003786 auth_config = auth.extract_auth_config_from_options(options)
3787
3788 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003789
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003790 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3791 if cl.IsGerrit():
3792 if options.message:
3793 # This could be implemented, but it requires sending a new patch to
3794 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3795 # Besides, Gerrit has the ability to change the commit message on submit
3796 # automatically, thus there is no need to support this option (so far?).
3797 parser.error('-m MESSAGE option is not supported for Gerrit.')
3798 if options.contributor:
3799 parser.error(
3800 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3801 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3802 'the contributor\'s "name <email>". If you can\'t upload such a '
3803 'commit for review, contact your repository admin and request'
3804 '"Forge-Author" permission.')
3805 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3806 options.verbose)
3807
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003808 current = cl.GetBranch()
3809 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3810 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07003811 print()
3812 print('Attempting to push branch %r into another local branch!' % current)
3813 print()
3814 print('Either reparent this branch on top of origin/master:')
3815 print(' git reparent-branch --root')
3816 print()
3817 print('OR run `git rebase-update` if you think the parent branch is ')
3818 print('already committed.')
3819 print()
3820 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003821 return 1
3822
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003823 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003824 # Default to merging against our best guess of the upstream branch.
3825 args = [cl.GetUpstreamBranch()]
3826
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003827 if options.contributor:
3828 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07003829 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003830 return 1
3831
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003832 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003833 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003834
sbc@chromium.org71437c02015-04-09 19:29:40 +00003835 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003836 return 1
3837
3838 # This rev-list syntax means "show all commits not in my branch that
3839 # are in base_branch".
3840 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3841 base_branch]).splitlines()
3842 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07003843 print('Base branch "%s" has %d commits '
3844 'not in this branch.' % (base_branch, len(upstream_commits)))
3845 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003846 return 1
3847
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003848 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003849 svn_head = None
3850 if cmd == 'dcommit' or base_has_submodules:
3851 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
3852 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003853
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003854 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003855 # If the base_head is a submodule merge commit, the first parent of the
3856 # base_head should be a git-svn commit, which is what we're interested in.
3857 base_svn_head = base_branch
3858 if base_has_submodules:
3859 base_svn_head += '^1'
3860
3861 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003862 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07003863 print('This branch has %d additional commits not upstreamed yet.'
3864 % len(extra_commits.splitlines()))
3865 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
3866 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003867 return 1
3868
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003869 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003870 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003871 author = None
3872 if options.contributor:
3873 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003874 hook_results = cl.RunHook(
3875 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003876 may_prompt=not options.force,
3877 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003878 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003879 if not hook_results.should_continue():
3880 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003881
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003882 # Check the tree status if the tree status URL is set.
3883 status = GetTreeStatus()
3884 if 'closed' == status:
3885 print('The tree is closed. Please wait for it to reopen. Use '
3886 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3887 return 1
3888 elif 'unknown' == status:
3889 print('Unable to determine tree status. Please verify manually and '
3890 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3891 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003892
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003893 change_desc = ChangeDescription(options.message)
3894 if not change_desc.description and cl.GetIssue():
3895 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003896
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003897 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00003898 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003899 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00003900 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003901 print('No description set.')
3902 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00003903 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003904
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003905 # Keep a separate copy for the commit message, because the commit message
3906 # contains the link to the Rietveld issue, while the Rietveld message contains
3907 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003908 # Keep a separate copy for the commit message.
3909 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00003910 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003911
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003912 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00003913 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00003914 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00003915 # after it. Add a period on a new line to circumvent this. Also add a space
3916 # before the period to make sure that Gitiles continues to correctly resolve
3917 # the URL.
3918 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003919 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003920 commit_desc.append_footer('Patch from %s.' % options.contributor)
3921
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00003922 print('Description:')
3923 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003924
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003925 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003926 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00003927 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003928
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003929 # We want to squash all this branch's commits into one commit with the proper
3930 # description. We do this by doing a "reset --soft" to the base branch (which
3931 # keeps the working copy the same), then dcommitting that. If origin/master
3932 # has a submodule merge commit, we'll also need to cherry-pick the squashed
3933 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003934 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003935 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
3936 # Delete the branches if they exist.
3937 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
3938 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
3939 result = RunGitWithCode(showref_cmd)
3940 if result[0] == 0:
3941 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003942
3943 # We might be in a directory that's present in this branch but not in the
3944 # trunk. Move up to the top of the tree so that git commands that expect a
3945 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003946 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003947 if rel_base_path:
3948 os.chdir(rel_base_path)
3949
3950 # Stuff our change into the merge branch.
3951 # We wrap in a try...finally block so if anything goes wrong,
3952 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003953 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003954 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003955 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003956 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003957 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00003958 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003959 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003960 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003961 RunGit(
3962 [
3963 'commit', '--author', options.contributor,
3964 '-m', commit_desc.description,
3965 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003966 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003967 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003968 if base_has_submodules:
3969 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
3970 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
3971 RunGit(['checkout', CHERRY_PICK_BRANCH])
3972 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003973 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003974 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003975 mirror = settings.GetGitMirror(remote)
3976 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003977 pending_prefix = settings.GetPendingRefPrefix()
3978 if not pending_prefix or branch.startswith(pending_prefix):
3979 # If not using refs/pending/heads/* at all, or target ref is already set
3980 # to pending, then push to the target ref directly.
3981 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003982 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003983 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003984 else:
3985 # Cherry-pick the change on top of pending ref and then push it.
3986 assert branch.startswith('refs/'), branch
3987 assert pending_prefix[-1] == '/', pending_prefix
3988 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003989 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003990 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003991 if retcode == 0:
3992 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003993 else:
3994 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003995 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003996 'svn', 'dcommit',
3997 '-C%s' % options.similarity,
3998 '--no-rebase', '--rmdir',
3999 ]
4000 if settings.GetForceHttpsCommitUrl():
4001 # Allow forcing https commit URLs for some projects that don't allow
4002 # committing to http URLs (like Google Code).
4003 remote_url = cl.GetGitSvnRemoteUrl()
4004 if urlparse.urlparse(remote_url).scheme == 'http':
4005 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004006 cmd_args.append('--commit-url=%s' % remote_url)
4007 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004008 if 'Committed r' in output:
4009 revision = re.match(
4010 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4011 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004012 finally:
4013 # And then swap back to the original branch and clean up.
4014 RunGit(['checkout', '-q', cl.GetBranch()])
4015 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004016 if base_has_submodules:
4017 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004018
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004019 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004020 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004021 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004022
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004023 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004024 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004025 try:
4026 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4027 # We set pushed_to_pending to False, since it made it all the way to the
4028 # real ref.
4029 pushed_to_pending = False
4030 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004031 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004032
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004033 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004034 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004035 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004036 if not to_pending:
4037 if viewvc_url and revision:
4038 change_desc.append_footer(
4039 'Committed: %s%s' % (viewvc_url, revision))
4040 elif revision:
4041 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004042 print('Closing issue '
4043 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004044 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004045 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004046 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004047 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004048 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004049 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004050 if options.bypass_hooks:
4051 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4052 else:
4053 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004054 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004055 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004056
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004057 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004058 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004059 print('The commit is in the pending queue (%s).' % pending_ref)
4060 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4061 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004062
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004063 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4064 if os.path.isfile(hook):
4065 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004066
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004067 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004068
4069
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004070def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004071 print()
4072 print('Waiting for commit to be landed on %s...' % real_ref)
4073 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004074 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4075 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004076 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004077
4078 loop = 0
4079 while True:
4080 sys.stdout.write('fetching (%d)... \r' % loop)
4081 sys.stdout.flush()
4082 loop += 1
4083
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004084 if mirror:
4085 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004086 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4087 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4088 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4089 for commit in commits.splitlines():
4090 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004091 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004092 return commit
4093
4094 current_rev = to_rev
4095
4096
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004097def PushToGitPending(remote, pending_ref, upstream_ref):
4098 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4099
4100 Returns:
4101 (retcode of last operation, output log of last operation).
4102 """
4103 assert pending_ref.startswith('refs/'), pending_ref
4104 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4105 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4106 code = 0
4107 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004108 max_attempts = 3
4109 attempts_left = max_attempts
4110 while attempts_left:
4111 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004112 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004113 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004114
4115 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004116 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004117 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004118 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004119 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004120 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004121 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004122 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004123 continue
4124
4125 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004126 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004127 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004128 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004129 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004130 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4131 'the following files have merge conflicts:' % pending_ref)
4132 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4133 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004134 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004135 return code, out
4136
4137 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004138 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004139 code, out = RunGitWithCode(
4140 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4141 if code == 0:
4142 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004143 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004144 return code, out
4145
vapiera7fbd5a2016-06-16 09:17:49 -07004146 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004147 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004148 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004149 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004150 print('Fatal push error. Make sure your .netrc credentials and git '
4151 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004152 return code, out
4153
vapiera7fbd5a2016-06-16 09:17:49 -07004154 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004155 return code, out
4156
4157
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004158def IsFatalPushFailure(push_stdout):
4159 """True if retrying push won't help."""
4160 return '(prohibited by Gerrit)' in push_stdout
4161
4162
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004163@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004164def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004165 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004166 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004167 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004168 # If it looks like previous commits were mirrored with git-svn.
4169 message = """This repository appears to be a git-svn mirror, but no
4170upstream SVN master is set. You probably need to run 'git auto-svn' once."""
4171 else:
4172 message = """This doesn't appear to be an SVN repository.
4173If your project has a true, writeable git repository, you probably want to run
4174'git cl land' instead.
4175If your project has a git mirror of an upstream SVN master, you probably need
4176to run 'git svn init'.
4177
4178Using the wrong command might cause your commit to appear to succeed, and the
4179review to be closed, without actually landing upstream. If you choose to
4180proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004181 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004182 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004183 return SendUpstream(parser, args, 'dcommit')
4184
4185
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004186@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004187def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004188 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004189 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004190 print('This appears to be an SVN repository.')
4191 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004192 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004193 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004194 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004195
4196
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004197@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004198def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004199 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004200 parser.add_option('-b', dest='newbranch',
4201 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004202 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004203 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004204 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4205 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004206 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004207 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004208 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004209 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004210 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004211 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004212
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004213
4214 group = optparse.OptionGroup(
4215 parser,
4216 'Options for continuing work on the current issue uploaded from a '
4217 'different clone (e.g. different machine). Must be used independently '
4218 'from the other options. No issue number should be specified, and the '
4219 'branch must have an issue number associated with it')
4220 group.add_option('--reapply', action='store_true', dest='reapply',
4221 help='Reset the branch and reapply the issue.\n'
4222 'CAUTION: This will undo any local changes in this '
4223 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004224
4225 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004226 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004227 parser.add_option_group(group)
4228
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004229 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004230 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004231 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004232 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004233 auth_config = auth.extract_auth_config_from_options(options)
4234
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004235
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004236 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004237 if options.newbranch:
4238 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004239 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004240 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004241
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004242 cl = Changelist(auth_config=auth_config,
4243 codereview=options.forced_codereview)
4244 if not cl.GetIssue():
4245 parser.error('current branch must have an associated issue')
4246
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004247 upstream = cl.GetUpstreamBranch()
4248 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004249 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004250
4251 RunGit(['reset', '--hard', upstream])
4252 if options.pull:
4253 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004254
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004255 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4256 options.directory)
4257
4258 if len(args) != 1 or not args[0]:
4259 parser.error('Must specify issue number or url')
4260
4261 # We don't want uncommitted changes mixed up with the patch.
4262 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004263 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004264
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004265 if options.newbranch:
4266 if options.force:
4267 RunGit(['branch', '-D', options.newbranch],
4268 stderr=subprocess2.PIPE, error_ok=True)
4269 RunGit(['new-branch', options.newbranch])
4270
4271 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4272
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004273 if cl.IsGerrit():
4274 if options.reject:
4275 parser.error('--reject is not supported with Gerrit codereview.')
4276 if options.nocommit:
4277 parser.error('--nocommit is not supported with Gerrit codereview.')
4278 if options.directory:
4279 parser.error('--directory is not supported with Gerrit codereview.')
4280
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004281 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004282 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004283
4284
4285def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004286 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004287 # Provide a wrapper for git svn rebase to help avoid accidental
4288 # git svn dcommit.
4289 # It's the only command that doesn't use parser at all since we just defer
4290 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004291
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004292 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004293
4294
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004295def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004296 """Fetches the tree status and returns either 'open', 'closed',
4297 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004298 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004299 if url:
4300 status = urllib2.urlopen(url).read().lower()
4301 if status.find('closed') != -1 or status == '0':
4302 return 'closed'
4303 elif status.find('open') != -1 or status == '1':
4304 return 'open'
4305 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004306 return 'unset'
4307
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004308
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004309def GetTreeStatusReason():
4310 """Fetches the tree status from a json url and returns the message
4311 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004312 url = settings.GetTreeStatusUrl()
4313 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004314 connection = urllib2.urlopen(json_url)
4315 status = json.loads(connection.read())
4316 connection.close()
4317 return status['message']
4318
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004319
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004320def GetBuilderMaster(bot_list):
4321 """For a given builder, fetch the master from AE if available."""
4322 map_url = 'https://builders-map.appspot.com/'
4323 try:
4324 master_map = json.load(urllib2.urlopen(map_url))
4325 except urllib2.URLError as e:
4326 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4327 (map_url, e))
4328 except ValueError as e:
4329 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4330 if not master_map:
4331 return None, 'Failed to build master map.'
4332
4333 result_master = ''
4334 for bot in bot_list:
4335 builder = bot.split(':', 1)[0]
4336 master_list = master_map.get(builder, [])
4337 if not master_list:
4338 return None, ('No matching master for builder %s.' % builder)
4339 elif len(master_list) > 1:
4340 return None, ('The builder name %s exists in multiple masters %s.' %
4341 (builder, master_list))
4342 else:
4343 cur_master = master_list[0]
4344 if not result_master:
4345 result_master = cur_master
4346 elif result_master != cur_master:
4347 return None, 'The builders do not belong to the same master.'
4348 return result_master, None
4349
4350
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004351def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004352 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004353 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004354 status = GetTreeStatus()
4355 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004356 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004357 return 2
4358
vapiera7fbd5a2016-06-16 09:17:49 -07004359 print('The tree is %s' % status)
4360 print()
4361 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004362 if status != 'open':
4363 return 1
4364 return 0
4365
4366
maruel@chromium.org15192402012-09-06 12:38:29 +00004367def CMDtry(parser, args):
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004368 """Triggers try jobs through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00004369 group = optparse.OptionGroup(parser, "Try job options")
4370 group.add_option(
4371 "-b", "--bot", action="append",
4372 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
4373 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004374 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00004375 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004376 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00004377 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004378 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00004379 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004380 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004381 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004382 "-r", "--revision",
4383 help="Revision to use for the try job; default: the "
4384 "revision will be determined by the try server; see "
4385 "its waterfall for more info")
4386 group.add_option(
4387 "-c", "--clobber", action="store_true", default=False,
4388 help="Force a clobber before building; e.g. don't do an "
4389 "incremental build")
4390 group.add_option(
4391 "--project",
4392 help="Override which project to use. Projects are defined "
4393 "server-side to define what default bot set to use")
4394 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004395 "-p", "--property", dest="properties", action="append", default=[],
4396 help="Specify generic properties in the form -p key1=value1 -p "
4397 "key2=value2 etc (buildbucket only). The value will be treated as "
4398 "json if decodable, or as string otherwise.")
4399 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004400 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004401 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004402 "--use-rietveld", action="store_true", default=False,
4403 help="Use Rietveld to trigger try jobs.")
4404 group.add_option(
4405 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4406 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004407 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004408 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004409 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004410 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004411
machenbach@chromium.org45453142015-09-15 08:45:22 +00004412 if options.use_rietveld and options.properties:
4413 parser.error('Properties can only be specified with buildbucket')
4414
4415 # Make sure that all properties are prop=value pairs.
4416 bad_params = [x for x in options.properties if '=' not in x]
4417 if bad_params:
4418 parser.error('Got properties with missing "=": %s' % bad_params)
4419
maruel@chromium.org15192402012-09-06 12:38:29 +00004420 if args:
4421 parser.error('Unknown arguments: %s' % args)
4422
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004423 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004424 if not cl.GetIssue():
4425 parser.error('Need to upload first')
4426
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004427 if cl.IsGerrit():
4428 parser.error(
4429 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4430 'If your project has Commit Queue, dry run is a workaround:\n'
4431 ' git cl set-commit --dry-run')
4432 # Code below assumes Rietveld issue.
4433 # TODO(tandrii): actually implement for Gerrit http://crbug.com/599931.
4434
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004435 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004436 if props.get('closed'):
4437 parser.error('Cannot send tryjobs for a closed CL')
4438
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004439 if props.get('private'):
4440 parser.error('Cannot use trybots with private issue')
4441
maruel@chromium.org15192402012-09-06 12:38:29 +00004442 if not options.name:
4443 options.name = cl.GetBranch()
4444
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004445 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004446 options.master, err_msg = GetBuilderMaster(options.bot)
4447 if err_msg:
4448 parser.error('Tryserver master cannot be found because: %s\n'
4449 'Please manually specify the tryserver master'
4450 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004451
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004452 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004453 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004454 if not options.bot:
4455 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004456
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004457 # Get try masters from PRESUBMIT.py files.
4458 masters = presubmit_support.DoGetTryMasters(
4459 change,
4460 change.LocalPaths(),
4461 settings.GetRoot(),
4462 None,
4463 None,
4464 options.verbose,
4465 sys.stdout)
4466 if masters:
4467 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004468
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004469 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4470 options.bot = presubmit_support.DoGetTrySlaves(
4471 change,
4472 change.LocalPaths(),
4473 settings.GetRoot(),
4474 None,
4475 None,
4476 options.verbose,
4477 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004478
4479 if not options.bot:
4480 # Get try masters from cq.cfg if any.
4481 # TODO(tandrii): some (but very few) projects store cq.cfg in different
4482 # location.
4483 cq_cfg = os.path.join(change.RepositoryRoot(),
4484 'infra', 'config', 'cq.cfg')
4485 if os.path.exists(cq_cfg):
4486 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00004487 cq_masters = commit_queue.get_master_builder_map(
4488 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004489 for master, builders in cq_masters.iteritems():
4490 for builder in builders:
4491 # Skip presubmit builders, because these will fail without LGTM.
machenbach@chromium.org2403e802016-04-29 12:34:42 +00004492 masters.setdefault(master, {})[builder] = ['defaulttests']
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004493 if masters:
tandriib93dd2b2016-06-07 08:03:08 -07004494 print('Loaded default bots from CQ config (%s)' % cq_cfg)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004495 return masters
tandriib93dd2b2016-06-07 08:03:08 -07004496 else:
4497 print('CQ config exists (%s) but has no try bots listed' % cq_cfg)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004498
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004499 if not options.bot:
4500 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00004501
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004502 builders_and_tests = {}
4503 # TODO(machenbach): The old style command-line options don't support
4504 # multiple try masters yet.
4505 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4506 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4507
4508 for bot in old_style:
4509 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004510 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004511 elif ',' in bot:
4512 parser.error('Specify one bot per --bot flag')
4513 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004514 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004515
4516 for bot, tests in new_style:
4517 builders_and_tests.setdefault(bot, []).extend(tests)
4518
4519 # Return a master map with one master to be backwards compatible. The
4520 # master name defaults to an empty string, which will cause the master
4521 # not to be set on rietveld (deprecated).
4522 return {options.master: builders_and_tests}
4523
4524 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004525
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004526 for builders in masters.itervalues():
4527 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004528 print('ERROR You are trying to send a job to a triggered bot. This type '
4529 'of bot requires an\ninitial job from a parent (usually a builder).'
4530 ' Instead send your job to the parent.\n'
4531 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004532 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004533
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004534 patchset = cl.GetMostRecentPatchset()
4535 if patchset and patchset != cl.GetPatchset():
4536 print(
4537 '\nWARNING Mismatch between local config and server. Did a previous '
4538 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4539 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004540 if options.luci:
4541 trigger_luci_job(cl, masters, options)
4542 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004543 try:
4544 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4545 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004546 print('ERROR: %s' % ex)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004547 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004548 except Exception as e:
4549 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
vapiera7fbd5a2016-06-16 09:17:49 -07004550 print('ERROR: Exception when trying to trigger tryjobs: %s\n%s' %
4551 (e, stacktrace))
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004552 return 1
4553 else:
4554 try:
4555 cl.RpcServer().trigger_distributed_try_jobs(
4556 cl.GetIssue(), patchset, options.name, options.clobber,
4557 options.revision, masters)
4558 except urllib2.HTTPError as e:
4559 if e.code == 404:
4560 print('404 from rietveld; '
4561 'did you mean to use "git try" instead of "git cl try"?')
4562 return 1
4563 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004564
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004565 for (master, builders) in sorted(masters.iteritems()):
4566 if master:
vapiera7fbd5a2016-06-16 09:17:49 -07004567 print('Master: %s' % master)
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004568 length = max(len(builder) for builder in builders)
4569 for builder in sorted(builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004570 print(' %*s: %s' % (length, builder, ','.join(builders[builder])))
maruel@chromium.org15192402012-09-06 12:38:29 +00004571 return 0
4572
4573
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004574def CMDtry_results(parser, args):
4575 group = optparse.OptionGroup(parser, "Try job results options")
4576 group.add_option(
4577 "-p", "--patchset", type=int, help="patchset number if not current.")
4578 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004579 "--print-master", action='store_true', help="print master name as well.")
4580 group.add_option(
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004581 "--color", action='store_true', default=setup_color.IS_TTY,
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004582 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004583 group.add_option(
4584 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4585 help="Host of buildbucket. The default host is %default.")
4586 parser.add_option_group(group)
4587 auth.add_auth_options(parser)
4588 options, args = parser.parse_args(args)
4589 if args:
4590 parser.error('Unrecognized args: %s' % ' '.join(args))
4591
4592 auth_config = auth.extract_auth_config_from_options(options)
4593 cl = Changelist(auth_config=auth_config)
4594 if not cl.GetIssue():
4595 parser.error('Need to upload first')
4596
4597 if not options.patchset:
4598 options.patchset = cl.GetMostRecentPatchset()
4599 if options.patchset and options.patchset != cl.GetPatchset():
4600 print(
4601 '\nWARNING Mismatch between local config and server. Did a previous '
4602 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4603 'Continuing using\npatchset %s.\n' % options.patchset)
4604 try:
4605 jobs = fetch_try_jobs(auth_config, cl, options)
4606 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004607 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004608 return 1
4609 except Exception as e:
4610 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
vapiera7fbd5a2016-06-16 09:17:49 -07004611 print('ERROR: Exception when trying to fetch tryjobs: %s\n%s' %
4612 (e, stacktrace))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004613 return 1
4614 print_tryjobs(options, jobs)
4615 return 0
4616
4617
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004618@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004619def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004620 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004621 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004622 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004623 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004624
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004625 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004626 if args:
4627 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004628 branch = cl.GetBranch()
4629 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004630 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004631 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004632
4633 # Clear configured merge-base, if there is one.
4634 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004635 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004636 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004637 return 0
4638
4639
thestig@chromium.org00858c82013-12-02 23:08:03 +00004640def CMDweb(parser, args):
4641 """Opens the current CL in the web browser."""
4642 _, args = parser.parse_args(args)
4643 if args:
4644 parser.error('Unrecognized args: %s' % ' '.join(args))
4645
4646 issue_url = Changelist().GetIssueURL()
4647 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004648 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004649 return 1
4650
4651 webbrowser.open(issue_url)
4652 return 0
4653
4654
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004655def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004656 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004657 parser.add_option('-d', '--dry-run', action='store_true',
4658 help='trigger in dry run mode')
4659 parser.add_option('-c', '--clear', action='store_true',
4660 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004661 auth.add_auth_options(parser)
4662 options, args = parser.parse_args(args)
4663 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004664 if args:
4665 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004666 if options.dry_run and options.clear:
4667 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4668
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004669 cl = Changelist(auth_config=auth_config)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004670 if options.clear:
4671 state = _CQState.CLEAR
4672 elif options.dry_run:
4673 state = _CQState.DRY_RUN
4674 else:
4675 state = _CQState.COMMIT
4676 if not cl.GetIssue():
4677 parser.error('Must upload the issue first')
4678 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004679 return 0
4680
4681
groby@chromium.org411034a2013-02-26 15:12:01 +00004682def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004683 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004684 auth.add_auth_options(parser)
4685 options, args = parser.parse_args(args)
4686 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004687 if args:
4688 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004689 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00004690 # Ensure there actually is an issue to close.
4691 cl.GetDescription()
4692 cl.CloseIssue()
4693 return 0
4694
4695
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004696def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004697 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004698 auth.add_auth_options(parser)
4699 options, args = parser.parse_args(args)
4700 auth_config = auth.extract_auth_config_from_options(options)
4701 if args:
4702 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004703
4704 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004705 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004706 # Staged changes would be committed along with the patch from last
4707 # upload, hence counted toward the "last upload" side in the final
4708 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004709 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004710 return 1
4711
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004712 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004713 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004714 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004715 if not issue:
4716 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004717 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004718 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004719
4720 # Create a new branch based on the merge-base
4721 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004722 # Clear cached branch in cl object, to avoid overwriting original CL branch
4723 # properties.
4724 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004725 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004726 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004727 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004728 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004729 return rtn
4730
wychen@chromium.org06928532015-02-03 02:11:29 +00004731 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004732 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00004733 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004734 finally:
4735 RunGit(['checkout', '-q', branch])
4736 RunGit(['branch', '-D', TMP_BRANCH])
4737
4738 return 0
4739
4740
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004741def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004742 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004743 parser.add_option(
4744 '--no-color',
4745 action='store_true',
4746 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004747 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004748 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004749 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004750
4751 author = RunGit(['config', 'user.email']).strip() or None
4752
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004753 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004754
4755 if args:
4756 if len(args) > 1:
4757 parser.error('Unknown args')
4758 base_branch = args[0]
4759 else:
4760 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004761 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004762
4763 change = cl.GetChange(base_branch, None)
4764 return owners_finder.OwnersFinder(
4765 [f.LocalPath() for f in
4766 cl.GetChange(base_branch, None).AffectedFiles()],
4767 change.RepositoryRoot(), author,
4768 fopen=file, os_path=os.path, glob=glob.glob,
4769 disable_color=options.no_color).run()
4770
4771
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004772def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004773 """Generates a diff command."""
4774 # Generate diff for the current branch's changes.
4775 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4776 upstream_commit, '--' ]
4777
4778 if args:
4779 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004780 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004781 diff_cmd.append(arg)
4782 else:
4783 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004784
4785 return diff_cmd
4786
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004787def MatchingFileType(file_name, extensions):
4788 """Returns true if the file name ends with one of the given extensions."""
4789 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004790
enne@chromium.org555cfe42014-01-29 18:21:39 +00004791@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004792def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004793 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00004794 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004795 GN_EXTS = ['.gn', '.gni']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004796 parser.add_option('--full', action='store_true',
4797 help='Reformat the full content of all touched files')
4798 parser.add_option('--dry-run', action='store_true',
4799 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004800 parser.add_option('--python', action='store_true',
4801 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004802 parser.add_option('--diff', action='store_true',
4803 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004804 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004805
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004806 # git diff generates paths against the root of the repository. Change
4807 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004808 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004809 if rel_base_path:
4810 os.chdir(rel_base_path)
4811
digit@chromium.org29e47272013-05-17 17:01:46 +00004812 # Grab the merge-base commit, i.e. the upstream commit of the current
4813 # branch when it was created or the last time it was rebased. This is
4814 # to cover the case where the user may have called "git fetch origin",
4815 # moving the origin branch to a newer commit, but hasn't rebased yet.
4816 upstream_commit = None
4817 cl = Changelist()
4818 upstream_branch = cl.GetUpstreamBranch()
4819 if upstream_branch:
4820 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4821 upstream_commit = upstream_commit.strip()
4822
4823 if not upstream_commit:
4824 DieWithError('Could not find base commit for this branch. '
4825 'Are you in detached state?')
4826
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004827 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4828 diff_output = RunGit(changed_files_cmd)
4829 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004830 # Filter out files deleted by this CL
4831 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004832
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004833 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4834 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4835 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004836 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004837
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004838 top_dir = os.path.normpath(
4839 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
4840
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004841 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4842 # formatted. This is used to block during the presubmit.
4843 return_value = 0
4844
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004845 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004846 # Locate the clang-format binary in the checkout
4847 try:
4848 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07004849 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004850 DieWithError(e)
4851
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004852 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004853 cmd = [clang_format_tool]
4854 if not opts.dry_run and not opts.diff:
4855 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004856 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004857 if opts.diff:
4858 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004859 else:
4860 env = os.environ.copy()
4861 env['PATH'] = str(os.path.dirname(clang_format_tool))
4862 try:
4863 script = clang_format.FindClangFormatScriptInChromiumTree(
4864 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07004865 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004866 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004867
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004868 cmd = [sys.executable, script, '-p0']
4869 if not opts.dry_run and not opts.diff:
4870 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004871
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004872 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
4873 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004874
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004875 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
4876 if opts.diff:
4877 sys.stdout.write(stdout)
4878 if opts.dry_run and len(stdout) > 0:
4879 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004880
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004881 # Similar code to above, but using yapf on .py files rather than clang-format
4882 # on C/C++ files
4883 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004884 yapf_tool = gclient_utils.FindExecutable('yapf')
4885 if yapf_tool is None:
4886 DieWithError('yapf not found in PATH')
4887
4888 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004889 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004890 cmd = [yapf_tool]
4891 if not opts.dry_run and not opts.diff:
4892 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004893 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004894 if opts.diff:
4895 sys.stdout.write(stdout)
4896 else:
4897 # TODO(sbc): yapf --lines mode still has some issues.
4898 # https://github.com/google/yapf/issues/154
4899 DieWithError('--python currently only works with --full')
4900
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004901 # Dart's formatter does not have the nice property of only operating on
4902 # modified chunks, so hard code full.
4903 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004904 try:
4905 command = [dart_format.FindDartFmtToolInChromiumTree()]
4906 if not opts.dry_run and not opts.diff:
4907 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004908 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004909
ppi@chromium.org6593d932016-03-03 15:41:15 +00004910 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004911 if opts.dry_run and stdout:
4912 return_value = 2
4913 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07004914 print('Warning: Unable to check Dart code formatting. Dart SDK not '
4915 'found in this checkout. Files in other languages are still '
4916 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004917
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004918 # Format GN build files. Always run on full build files for canonical form.
4919 if gn_diff_files:
4920 cmd = ['gn', 'format']
4921 if not opts.dry_run and not opts.diff:
4922 cmd.append('--in-place')
4923 for gn_diff_file in gn_diff_files:
bsep@chromium.org627d9002016-04-29 00:00:52 +00004924 stdout = RunCommand(cmd + [gn_diff_file],
4925 shell=sys.platform == 'win32',
4926 cwd=top_dir)
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004927 if opts.diff:
4928 sys.stdout.write(stdout)
4929
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004930 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004931
4932
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004933@subcommand.usage('<codereview url or issue id>')
4934def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004935 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004936 _, args = parser.parse_args(args)
4937
4938 if len(args) != 1:
4939 parser.print_help()
4940 return 1
4941
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004942 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00004943 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004944 parser.print_help()
4945 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00004946 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004947
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004948 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00004949 output = RunGit(['config', '--local', '--get-regexp',
4950 r'branch\..*\.%s' % issueprefix],
4951 error_ok=True)
4952 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004953 if issue == target_issue:
4954 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004955
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004956 branches = []
4957 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00004958 branches.extend(find_issues(cls.IssueSettingSuffix()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004959 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07004960 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004961 return 1
4962 if len(branches) == 1:
4963 RunGit(['checkout', branches[0]])
4964 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004965 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004966 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07004967 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004968 which = raw_input('Choose by index: ')
4969 try:
4970 RunGit(['checkout', branches[int(which)]])
4971 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07004972 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004973 return 1
4974
4975 return 0
4976
4977
maruel@chromium.org29404b52014-09-08 22:58:00 +00004978def CMDlol(parser, args):
4979 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07004980 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00004981 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
4982 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
4983 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07004984 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00004985 return 0
4986
4987
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004988class OptionParser(optparse.OptionParser):
4989 """Creates the option parse and add --verbose support."""
4990 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004991 optparse.OptionParser.__init__(
4992 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004993 self.add_option(
4994 '-v', '--verbose', action='count', default=0,
4995 help='Use 2 times for more debugging info')
4996
4997 def parse_args(self, args=None, values=None):
4998 options, args = optparse.OptionParser.parse_args(self, args, values)
4999 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5000 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5001 return options, args
5002
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005003
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005004def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005005 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005006 print('\nYour python version %s is unsupported, please upgrade.\n' %
5007 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005008 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005009
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005010 # Reload settings.
5011 global settings
5012 settings = Settings()
5013
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005014 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005015 dispatcher = subcommand.CommandDispatcher(__name__)
5016 try:
5017 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005018 except auth.AuthenticationError as e:
5019 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005020 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005021 if e.code != 500:
5022 raise
5023 DieWithError(
5024 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5025 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005026 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005027
5028
5029if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005030 # These affect sys.stdout so do it outside of main() to simplify mocks in
5031 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005032 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005033 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005034 try:
5035 sys.exit(main(sys.argv[1:]))
5036 except KeyboardInterrupt:
5037 sys.stderr.write('interrupted\n')
5038 sys.exit(1)