blob: 0aafa85e1f73cc097c945569a7671025dc5201b4 [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:
tandrii8dd81ea2016-06-16 13:24:23 -07002837 RunGit(['config', 'gerrit.squash-uploads',
2838 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002839
tandrii@chromium.org28253532016-04-14 13:46:56 +00002840 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00002841 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00002842 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
2843
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002844 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2845 #should be of the form
2846 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2847 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2848 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2849 keyvals['ORIGIN_URL_CONFIG']])
2850
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002851
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002852def urlretrieve(source, destination):
2853 """urllib is broken for SSL connections via a proxy therefore we
2854 can't use urllib.urlretrieve()."""
2855 with open(destination, 'w') as f:
2856 f.write(urllib2.urlopen(source).read())
2857
2858
ukai@chromium.org712d6102013-11-27 00:52:58 +00002859def hasSheBang(fname):
2860 """Checks fname is a #! script."""
2861 with open(fname) as f:
2862 return f.read(2).startswith('#!')
2863
2864
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00002865# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
2866def DownloadHooks(*args, **kwargs):
2867 pass
2868
2869
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002870def DownloadGerritHook(force):
2871 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002872
2873 Args:
2874 force: True to update hooks. False to install hooks if not present.
2875 """
2876 if not settings.GetIsGerrit():
2877 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00002878 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002879 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2880 if not os.access(dst, os.X_OK):
2881 if os.path.exists(dst):
2882 if not force:
2883 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002884 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002885 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002886 if not hasSheBang(dst):
2887 DieWithError('Not a script: %s\n'
2888 'You need to download from\n%s\n'
2889 'into .git/hooks/commit-msg and '
2890 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002891 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2892 except Exception:
2893 if os.path.exists(dst):
2894 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002895 DieWithError('\nFailed to download hooks.\n'
2896 'You need to download from\n%s\n'
2897 'into .git/hooks/commit-msg and '
2898 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002899
2900
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002901
2902def GetRietveldCodereviewSettingsInteractively():
2903 """Prompt the user for settings."""
2904 server = settings.GetDefaultServerUrl(error_ok=True)
2905 prompt = 'Rietveld server (host[:port])'
2906 prompt += ' [%s]' % (server or DEFAULT_SERVER)
2907 newserver = ask_for_data(prompt + ':')
2908 if not server and not newserver:
2909 newserver = DEFAULT_SERVER
2910 if newserver:
2911 newserver = gclient_utils.UpgradeToHttps(newserver)
2912 if newserver != server:
2913 RunGit(['config', 'rietveld.server', newserver])
2914
2915 def SetProperty(initial, caption, name, is_url):
2916 prompt = caption
2917 if initial:
2918 prompt += ' ("x" to clear) [%s]' % initial
2919 new_val = ask_for_data(prompt + ':')
2920 if new_val == 'x':
2921 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
2922 elif new_val:
2923 if is_url:
2924 new_val = gclient_utils.UpgradeToHttps(new_val)
2925 if new_val != initial:
2926 RunGit(['config', 'rietveld.' + name, new_val])
2927
2928 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
2929 SetProperty(settings.GetDefaultPrivateFlag(),
2930 'Private flag (rietveld only)', 'private', False)
2931 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
2932 'tree-status-url', False)
2933 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
2934 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
2935 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
2936 'run-post-upload-hook', False)
2937
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002938@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002939def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002940 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002941
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002942 print('WARNING: git cl config works for Rietveld only.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002943 'For Gerrit, see http://crbug.com/603116.')
2944 # TODO(tandrii): add Gerrit support as part of http://crbug.com/603116.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00002945 parser.add_option('--activate-update', action='store_true',
2946 help='activate auto-updating [rietveld] section in '
2947 '.git/config')
2948 parser.add_option('--deactivate-update', action='store_true',
2949 help='deactivate auto-updating [rietveld] section in '
2950 '.git/config')
2951 options, args = parser.parse_args(args)
2952
2953 if options.deactivate_update:
2954 RunGit(['config', 'rietveld.autoupdate', 'false'])
2955 return
2956
2957 if options.activate_update:
2958 RunGit(['config', '--unset', 'rietveld.autoupdate'])
2959 return
2960
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002961 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002962 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002963 return 0
2964
2965 url = args[0]
2966 if not url.endswith('codereview.settings'):
2967 url = os.path.join(url, 'codereview.settings')
2968
2969 # Load code review settings and download hooks (if available).
2970 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
2971 return 0
2972
2973
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002974def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002975 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002976 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
2977 branch = ShortBranchName(branchref)
2978 _, args = parser.parse_args(args)
2979 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07002980 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002981 return RunGit(['config', 'branch.%s.base-url' % branch],
2982 error_ok=False).strip()
2983 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002984 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002985 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
2986 error_ok=False).strip()
2987
2988
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002989def color_for_status(status):
2990 """Maps a Changelist status to color, for CMDstatus and other tools."""
2991 return {
2992 'unsent': Fore.RED,
2993 'waiting': Fore.BLUE,
2994 'reply': Fore.YELLOW,
2995 'lgtm': Fore.GREEN,
2996 'commit': Fore.MAGENTA,
2997 'closed': Fore.CYAN,
2998 'error': Fore.WHITE,
2999 }.get(status, Fore.WHITE)
3000
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003001
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003002def get_cl_statuses(changes, fine_grained, max_processes=None):
3003 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003004
3005 If fine_grained is true, this will fetch CL statuses from the server.
3006 Otherwise, simply indicate if there's a matching url for the given branches.
3007
3008 If max_processes is specified, it is used as the maximum number of processes
3009 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3010 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003011
3012 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003013 """
3014 # Silence upload.py otherwise it becomes unwieldly.
3015 upload.verbosity = 0
3016
3017 if fine_grained:
3018 # Process one branch synchronously to work through authentication, then
3019 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003020 if changes:
3021 fetch = lambda cl: (cl, cl.GetStatus())
3022 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003023
kmarshall3bff56b2016-06-06 18:31:47 -07003024 if not changes:
3025 # Exit early if there was only one branch to fetch.
3026 return
3027
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003028 changes_to_fetch = changes[1:]
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003029 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003030 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003031 if max_processes is not None
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003032 else len(changes_to_fetch))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003033
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003034 fetched_cls = set()
3035 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003036 while True:
3037 try:
3038 row = it.next(timeout=5)
3039 except multiprocessing.TimeoutError:
3040 break
3041
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003042 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003043 yield row
3044
3045 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003046 for cl in set(changes_to_fetch) - fetched_cls:
3047 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003048
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003049 else:
3050 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003051 for cl in changes:
3052 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003053
rmistry@google.com2dd99862015-06-22 12:22:18 +00003054
3055def upload_branch_deps(cl, args):
3056 """Uploads CLs of local branches that are dependents of the current branch.
3057
3058 If the local branch dependency tree looks like:
3059 test1 -> test2.1 -> test3.1
3060 -> test3.2
3061 -> test2.2 -> test3.3
3062
3063 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3064 run on the dependent branches in this order:
3065 test2.1, test3.1, test3.2, test2.2, test3.3
3066
3067 Note: This function does not rebase your local dependent branches. Use it when
3068 you make a change to the parent branch that will not conflict with its
3069 dependent branches, and you would like their dependencies updated in
3070 Rietveld.
3071 """
3072 if git_common.is_dirty_git_tree('upload-branch-deps'):
3073 return 1
3074
3075 root_branch = cl.GetBranch()
3076 if root_branch is None:
3077 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3078 'Get on a branch!')
3079 if not cl.GetIssue() or not cl.GetPatchset():
3080 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3081 'patchset dependencies without an uploaded CL.')
3082
3083 branches = RunGit(['for-each-ref',
3084 '--format=%(refname:short) %(upstream:short)',
3085 'refs/heads'])
3086 if not branches:
3087 print('No local branches found.')
3088 return 0
3089
3090 # Create a dictionary of all local branches to the branches that are dependent
3091 # on it.
3092 tracked_to_dependents = collections.defaultdict(list)
3093 for b in branches.splitlines():
3094 tokens = b.split()
3095 if len(tokens) == 2:
3096 branch_name, tracked = tokens
3097 tracked_to_dependents[tracked].append(branch_name)
3098
vapiera7fbd5a2016-06-16 09:17:49 -07003099 print()
3100 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003101 dependents = []
3102 def traverse_dependents_preorder(branch, padding=''):
3103 dependents_to_process = tracked_to_dependents.get(branch, [])
3104 padding += ' '
3105 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003106 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003107 dependents.append(dependent)
3108 traverse_dependents_preorder(dependent, padding)
3109 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003110 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003111
3112 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003113 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003114 return 0
3115
vapiera7fbd5a2016-06-16 09:17:49 -07003116 print('This command will checkout all dependent branches and run '
3117 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003118 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3119
andybons@chromium.org962f9462016-02-03 20:00:42 +00003120 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003121 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003122 args.extend(['-t', 'Updated patchset dependency'])
3123
rmistry@google.com2dd99862015-06-22 12:22:18 +00003124 # Record all dependents that failed to upload.
3125 failures = {}
3126 # Go through all dependents, checkout the branch and upload.
3127 try:
3128 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003129 print()
3130 print('--------------------------------------')
3131 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003132 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003133 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003134 try:
3135 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003136 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003137 failures[dependent_branch] = 1
3138 except: # pylint: disable=W0702
3139 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003140 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003141 finally:
3142 # Swap back to the original root branch.
3143 RunGit(['checkout', '-q', root_branch])
3144
vapiera7fbd5a2016-06-16 09:17:49 -07003145 print()
3146 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003147 for dependent_branch in dependents:
3148 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003149 print(' %s : %s' % (dependent_branch, upload_status))
3150 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003151
3152 return 0
3153
3154
kmarshall3bff56b2016-06-06 18:31:47 -07003155def CMDarchive(parser, args):
3156 """Archives and deletes branches associated with closed changelists."""
3157 parser.add_option(
3158 '-j', '--maxjobs', action='store', type=int,
3159 help='The maximum number of jobs to use when retrieving review status')
3160 parser.add_option(
3161 '-f', '--force', action='store_true',
3162 help='Bypasses the confirmation prompt.')
3163
3164 auth.add_auth_options(parser)
3165 options, args = parser.parse_args(args)
3166 if args:
3167 parser.error('Unsupported args: %s' % ' '.join(args))
3168 auth_config = auth.extract_auth_config_from_options(options)
3169
3170 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3171 if not branches:
3172 return 0
3173
vapiera7fbd5a2016-06-16 09:17:49 -07003174 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003175 changes = [Changelist(branchref=b, auth_config=auth_config)
3176 for b in branches.splitlines()]
3177 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3178 statuses = get_cl_statuses(changes,
3179 fine_grained=True,
3180 max_processes=options.maxjobs)
3181 proposal = [(cl.GetBranch(),
3182 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3183 for cl, status in statuses
3184 if status == 'closed']
3185 proposal.sort()
3186
3187 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003188 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003189 return 0
3190
3191 current_branch = GetCurrentBranch()
3192
vapiera7fbd5a2016-06-16 09:17:49 -07003193 print('\nBranches with closed issues that will be archived:\n')
3194 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
kmarshall3bff56b2016-06-06 18:31:47 -07003195 for next_item in proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003196 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003197
3198 if any(branch == current_branch for branch, _ in proposal):
3199 print('You are currently on a branch \'%s\' which is associated with a '
3200 'closed codereview issue, so archive cannot proceed. Please '
3201 'checkout another branch and run this command again.' %
3202 current_branch)
3203 return 1
3204
3205 if not options.force:
3206 if ask_for_data('\nProceed with deletion (Y/N)? ').lower() != 'y':
vapiera7fbd5a2016-06-16 09:17:49 -07003207 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003208 return 1
3209
3210 for branch, tagname in proposal:
3211 RunGit(['tag', tagname, branch])
3212 RunGit(['branch', '-D', branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003213 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003214
3215 return 0
3216
3217
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003218def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003219 """Show status of changelists.
3220
3221 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003222 - Red not sent for review or broken
3223 - Blue waiting for review
3224 - Yellow waiting for you to reply to review
3225 - Green LGTM'ed
3226 - Magenta in the commit queue
3227 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003228
3229 Also see 'git cl comments'.
3230 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003231 parser.add_option('--field',
3232 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003233 parser.add_option('-f', '--fast', action='store_true',
3234 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003235 parser.add_option(
3236 '-j', '--maxjobs', action='store', type=int,
3237 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003238
3239 auth.add_auth_options(parser)
3240 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003241 if args:
3242 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003243 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003244
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003245 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003246 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003247 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003248 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003249 elif options.field == 'id':
3250 issueid = cl.GetIssue()
3251 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003252 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003253 elif options.field == 'patch':
3254 patchset = cl.GetPatchset()
3255 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003256 print(patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003257 elif options.field == 'url':
3258 url = cl.GetIssueURL()
3259 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003260 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003261 return 0
3262
3263 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3264 if not branches:
3265 print('No local branch found.')
3266 return 0
3267
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003268 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003269 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003270 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003271 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003272 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003273 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003274 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003275
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003276 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003277 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3278 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3279 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003280 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003281 c, status = output.next()
3282 branch_statuses[c.GetBranch()] = status
3283 status = branch_statuses.pop(branch)
3284 url = cl.GetIssueURL()
3285 if url and (not status or status == 'error'):
3286 # The issue probably doesn't exist anymore.
3287 url += ' (broken)'
3288
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003289 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003290 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003291 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003292 color = ''
3293 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003294 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003295 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003296 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003297 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003298
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003299 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003300 print()
3301 print('Current branch:',)
3302 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003303 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003304 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003305 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003306 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003307 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003308 print('Issue description:')
3309 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003310 return 0
3311
3312
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003313def colorize_CMDstatus_doc():
3314 """To be called once in main() to add colors to git cl status help."""
3315 colors = [i for i in dir(Fore) if i[0].isupper()]
3316
3317 def colorize_line(line):
3318 for color in colors:
3319 if color in line.upper():
3320 # Extract whitespaces first and the leading '-'.
3321 indent = len(line) - len(line.lstrip(' ')) + 1
3322 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3323 return line
3324
3325 lines = CMDstatus.__doc__.splitlines()
3326 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3327
3328
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003329@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003330def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003331 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003332
3333 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003334 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003335 parser.add_option('-r', '--reverse', action='store_true',
3336 help='Lookup the branch(es) for the specified issues. If '
3337 'no issues are specified, all branches with mapped '
3338 'issues will be listed.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003339 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003340 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003341 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003342
dnj@chromium.org406c4402015-03-03 17:22:28 +00003343 if options.reverse:
3344 branches = RunGit(['for-each-ref', 'refs/heads',
3345 '--format=%(refname:short)']).splitlines()
3346
3347 # Reverse issue lookup.
3348 issue_branch_map = {}
3349 for branch in branches:
3350 cl = Changelist(branchref=branch)
3351 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3352 if not args:
3353 args = sorted(issue_branch_map.iterkeys())
3354 for issue in args:
3355 if not issue:
3356 continue
vapiera7fbd5a2016-06-16 09:17:49 -07003357 print('Branch for issue number %s: %s' % (
3358 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
dnj@chromium.org406c4402015-03-03 17:22:28 +00003359 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003360 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003361 if len(args) > 0:
3362 try:
3363 issue = int(args[0])
3364 except ValueError:
3365 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003366 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003367 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003368 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003369 return 0
3370
3371
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003372def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003373 """Shows or posts review comments for any changelist."""
3374 parser.add_option('-a', '--add-comment', dest='comment',
3375 help='comment to add to an issue')
3376 parser.add_option('-i', dest='issue',
3377 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003378 parser.add_option('-j', '--json-file',
3379 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003380 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003381 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003382 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003383
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003384 issue = None
3385 if options.issue:
3386 try:
3387 issue = int(options.issue)
3388 except ValueError:
3389 DieWithError('A review issue id is expected to be a number')
3390
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003391 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003392
3393 if options.comment:
3394 cl.AddComment(options.comment)
3395 return 0
3396
3397 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003398 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003399 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003400 summary.append({
3401 'date': message['date'],
3402 'lgtm': False,
3403 'message': message['text'],
3404 'not_lgtm': False,
3405 'sender': message['sender'],
3406 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003407 if message['disapproval']:
3408 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003409 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003410 elif message['approval']:
3411 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003412 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003413 elif message['sender'] == data['owner_email']:
3414 color = Fore.MAGENTA
3415 else:
3416 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003417 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003418 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003419 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003420 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003421 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003422 if options.json_file:
3423 with open(options.json_file, 'wb') as f:
3424 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003425 return 0
3426
3427
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003428@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003429def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003430 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003431 parser.add_option('-d', '--display', action='store_true',
3432 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003433 parser.add_option('-n', '--new-description',
3434 help='New description to set for this issue (- for stdin)')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003435
3436 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003437 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003438 options, args = parser.parse_args(args)
3439 _process_codereview_select_options(parser, options)
3440
3441 target_issue = None
3442 if len(args) > 0:
3443 issue_arg = ParseIssueNumberArgument(args[0])
3444 if not issue_arg.valid:
3445 parser.print_help()
3446 return 1
3447 target_issue = issue_arg.issue
3448
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003449 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003450
3451 cl = Changelist(
3452 auth_config=auth_config, issue=target_issue,
3453 codereview=options.forced_codereview)
3454
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003455 if not cl.GetIssue():
3456 DieWithError('This branch has no associated changelist.')
3457 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003458
smut@google.com34fb6b12015-07-13 20:03:26 +00003459 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003460 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003461 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003462
3463 if options.new_description:
3464 text = options.new_description
3465 if text == '-':
3466 text = '\n'.join(l.rstrip() for l in sys.stdin)
3467
3468 description.set_description(text)
3469 else:
3470 description.prompt()
3471
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003472 if cl.GetDescription() != description.description:
3473 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003474 return 0
3475
3476
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003477def CreateDescriptionFromLog(args):
3478 """Pulls out the commit log to use as a base for the CL description."""
3479 log_args = []
3480 if len(args) == 1 and not args[0].endswith('.'):
3481 log_args = [args[0] + '..']
3482 elif len(args) == 1 and args[0].endswith('...'):
3483 log_args = [args[0][:-1]]
3484 elif len(args) == 2:
3485 log_args = [args[0] + '..' + args[1]]
3486 else:
3487 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003488 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003489
3490
thestig@chromium.org44202a22014-03-11 19:22:18 +00003491def CMDlint(parser, args):
3492 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003493 parser.add_option('--filter', action='append', metavar='-x,+y',
3494 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003495 auth.add_auth_options(parser)
3496 options, args = parser.parse_args(args)
3497 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003498
3499 # Access to a protected member _XX of a client class
3500 # pylint: disable=W0212
3501 try:
3502 import cpplint
3503 import cpplint_chromium
3504 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003505 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003506 return 1
3507
3508 # Change the current working directory before calling lint so that it
3509 # shows the correct base.
3510 previous_cwd = os.getcwd()
3511 os.chdir(settings.GetRoot())
3512 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003513 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003514 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3515 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003516 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003517 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003518 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003519
3520 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003521 command = args + files
3522 if options.filter:
3523 command = ['--filter=' + ','.join(options.filter)] + command
3524 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003525
3526 white_regex = re.compile(settings.GetLintRegex())
3527 black_regex = re.compile(settings.GetLintIgnoreRegex())
3528 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3529 for filename in filenames:
3530 if white_regex.match(filename):
3531 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07003532 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003533 else:
3534 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3535 extra_check_functions)
3536 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003537 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003538 finally:
3539 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07003540 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003541 if cpplint._cpplint_state.error_count != 0:
3542 return 1
3543 return 0
3544
3545
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003546def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003547 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003548 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003549 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003550 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003551 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003552 auth.add_auth_options(parser)
3553 options, args = parser.parse_args(args)
3554 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003555
sbc@chromium.org71437c02015-04-09 19:29:40 +00003556 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07003557 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003558 return 1
3559
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003560 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003561 if args:
3562 base_branch = args[0]
3563 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003564 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003565 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003566
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003567 cl.RunHook(
3568 committing=not options.upload,
3569 may_prompt=False,
3570 verbose=options.verbose,
3571 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003572 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003573
3574
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003575def GenerateGerritChangeId(message):
3576 """Returns Ixxxxxx...xxx change id.
3577
3578 Works the same way as
3579 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3580 but can be called on demand on all platforms.
3581
3582 The basic idea is to generate git hash of a state of the tree, original commit
3583 message, author/committer info and timestamps.
3584 """
3585 lines = []
3586 tree_hash = RunGitSilent(['write-tree'])
3587 lines.append('tree %s' % tree_hash.strip())
3588 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3589 if code == 0:
3590 lines.append('parent %s' % parent.strip())
3591 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3592 lines.append('author %s' % author.strip())
3593 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3594 lines.append('committer %s' % committer.strip())
3595 lines.append('')
3596 # Note: Gerrit's commit-hook actually cleans message of some lines and
3597 # whitespace. This code is not doing this, but it clearly won't decrease
3598 # entropy.
3599 lines.append(message)
3600 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3601 stdin='\n'.join(lines))
3602 return 'I%s' % change_hash.strip()
3603
3604
wittman@chromium.org455dc922015-01-26 20:15:50 +00003605def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3606 """Computes the remote branch ref to use for the CL.
3607
3608 Args:
3609 remote (str): The git remote for the CL.
3610 remote_branch (str): The git remote branch for the CL.
3611 target_branch (str): The target branch specified by the user.
3612 pending_prefix (str): The pending prefix from the settings.
3613 """
3614 if not (remote and remote_branch):
3615 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003616
wittman@chromium.org455dc922015-01-26 20:15:50 +00003617 if target_branch:
3618 # Cannonicalize branch references to the equivalent local full symbolic
3619 # refs, which are then translated into the remote full symbolic refs
3620 # below.
3621 if '/' not in target_branch:
3622 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3623 else:
3624 prefix_replacements = (
3625 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3626 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3627 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3628 )
3629 match = None
3630 for regex, replacement in prefix_replacements:
3631 match = re.search(regex, target_branch)
3632 if match:
3633 remote_branch = target_branch.replace(match.group(0), replacement)
3634 break
3635 if not match:
3636 # This is a branch path but not one we recognize; use as-is.
3637 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003638 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3639 # Handle the refs that need to land in different refs.
3640 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003641
wittman@chromium.org455dc922015-01-26 20:15:50 +00003642 # Create the true path to the remote branch.
3643 # Does the following translation:
3644 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3645 # * refs/remotes/origin/master -> refs/heads/master
3646 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3647 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3648 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3649 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3650 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3651 'refs/heads/')
3652 elif remote_branch.startswith('refs/remotes/branch-heads'):
3653 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3654 # If a pending prefix exists then replace refs/ with it.
3655 if pending_prefix:
3656 remote_branch = remote_branch.replace('refs/', pending_prefix)
3657 return remote_branch
3658
3659
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003660def cleanup_list(l):
3661 """Fixes a list so that comma separated items are put as individual items.
3662
3663 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3664 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3665 """
3666 items = sum((i.split(',') for i in l), [])
3667 stripped_items = (i.strip() for i in items)
3668 return sorted(filter(None, stripped_items))
3669
3670
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003671@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003672def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003673 """Uploads the current changelist to codereview.
3674
3675 Can skip dependency patchset uploads for a branch by running:
3676 git config branch.branch_name.skip-deps-uploads True
3677 To unset run:
3678 git config --unset branch.branch_name.skip-deps-uploads
3679 Can also set the above globally by using the --global flag.
3680 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003681 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3682 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003683 parser.add_option('--bypass-watchlists', action='store_true',
3684 dest='bypass_watchlists',
3685 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003686 parser.add_option('-f', action='store_true', dest='force',
3687 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003688 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003689 parser.add_option('-t', dest='title',
3690 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003691 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003692 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003693 help='reviewer email addresses')
3694 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003695 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003696 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003697 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003698 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003699 parser.add_option('--emulate_svn_auto_props',
3700 '--emulate-svn-auto-props',
3701 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003702 dest="emulate_svn_auto_props",
3703 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003704 parser.add_option('-c', '--use-commit-queue', action='store_true',
3705 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003706 parser.add_option('--private', action='store_true',
3707 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003708 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003709 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003710 metavar='TARGET',
3711 help='Apply CL to remote ref TARGET. ' +
3712 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003713 parser.add_option('--squash', action='store_true',
3714 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003715 parser.add_option('--no-squash', action='store_true',
3716 help='Don\'t squash multiple commits into one ' +
3717 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003718 parser.add_option('--email', default=None,
3719 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003720 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3721 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003722 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3723 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003724 help='Send the patchset to do a CQ dry run right after '
3725 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003726 parser.add_option('--dependencies', action='store_true',
3727 help='Uploads CLs of all the local branches that depend on '
3728 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003729
rmistry@google.com2dd99862015-06-22 12:22:18 +00003730 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003731 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003732 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003733 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003734 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003735 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003736 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003737
sbc@chromium.org71437c02015-04-09 19:29:40 +00003738 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003739 return 1
3740
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003741 options.reviewers = cleanup_list(options.reviewers)
3742 options.cc = cleanup_list(options.cc)
3743
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003744 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3745 settings.GetIsGerrit()
3746
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003747 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00003748 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003749
3750
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003751def IsSubmoduleMergeCommit(ref):
3752 # When submodules are added to the repo, we expect there to be a single
3753 # non-git-svn merge commit at remote HEAD with a signature comment.
3754 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003755 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003756 return RunGit(cmd) != ''
3757
3758
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003759def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003760 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003761
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003762 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3763 upstream and closes the issue automatically and atomically.
3764
3765 Otherwise (in case of Rietveld):
3766 Squashes branch into a single commit.
3767 Updates changelog with metadata (e.g. pointer to review).
3768 Pushes/dcommits the code upstream.
3769 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003770 """
3771 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3772 help='bypass upload presubmit hook')
3773 parser.add_option('-m', dest='message',
3774 help="override review description")
3775 parser.add_option('-f', action='store_true', dest='force',
3776 help="force yes to questions (don't prompt)")
3777 parser.add_option('-c', dest='contributor',
3778 help="external contributor for patch (appended to " +
3779 "description and used as author for git). Should be " +
3780 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003781 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003782 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003783 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003784 auth_config = auth.extract_auth_config_from_options(options)
3785
3786 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003787
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003788 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3789 if cl.IsGerrit():
3790 if options.message:
3791 # This could be implemented, but it requires sending a new patch to
3792 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3793 # Besides, Gerrit has the ability to change the commit message on submit
3794 # automatically, thus there is no need to support this option (so far?).
3795 parser.error('-m MESSAGE option is not supported for Gerrit.')
3796 if options.contributor:
3797 parser.error(
3798 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3799 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3800 'the contributor\'s "name <email>". If you can\'t upload such a '
3801 'commit for review, contact your repository admin and request'
3802 '"Forge-Author" permission.')
3803 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3804 options.verbose)
3805
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003806 current = cl.GetBranch()
3807 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3808 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07003809 print()
3810 print('Attempting to push branch %r into another local branch!' % current)
3811 print()
3812 print('Either reparent this branch on top of origin/master:')
3813 print(' git reparent-branch --root')
3814 print()
3815 print('OR run `git rebase-update` if you think the parent branch is ')
3816 print('already committed.')
3817 print()
3818 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003819 return 1
3820
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003821 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003822 # Default to merging against our best guess of the upstream branch.
3823 args = [cl.GetUpstreamBranch()]
3824
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003825 if options.contributor:
3826 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07003827 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003828 return 1
3829
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003830 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003831 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003832
sbc@chromium.org71437c02015-04-09 19:29:40 +00003833 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003834 return 1
3835
3836 # This rev-list syntax means "show all commits not in my branch that
3837 # are in base_branch".
3838 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3839 base_branch]).splitlines()
3840 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07003841 print('Base branch "%s" has %d commits '
3842 'not in this branch.' % (base_branch, len(upstream_commits)))
3843 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003844 return 1
3845
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003846 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003847 svn_head = None
3848 if cmd == 'dcommit' or base_has_submodules:
3849 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
3850 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003851
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003852 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003853 # If the base_head is a submodule merge commit, the first parent of the
3854 # base_head should be a git-svn commit, which is what we're interested in.
3855 base_svn_head = base_branch
3856 if base_has_submodules:
3857 base_svn_head += '^1'
3858
3859 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003860 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07003861 print('This branch has %d additional commits not upstreamed yet.'
3862 % len(extra_commits.splitlines()))
3863 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
3864 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003865 return 1
3866
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003867 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003868 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003869 author = None
3870 if options.contributor:
3871 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003872 hook_results = cl.RunHook(
3873 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003874 may_prompt=not options.force,
3875 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003876 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003877 if not hook_results.should_continue():
3878 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003879
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003880 # Check the tree status if the tree status URL is set.
3881 status = GetTreeStatus()
3882 if 'closed' == status:
3883 print('The tree is closed. Please wait for it to reopen. Use '
3884 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3885 return 1
3886 elif 'unknown' == status:
3887 print('Unable to determine tree status. Please verify manually and '
3888 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3889 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003890
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003891 change_desc = ChangeDescription(options.message)
3892 if not change_desc.description and cl.GetIssue():
3893 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003894
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003895 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00003896 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003897 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00003898 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003899 print('No description set.')
3900 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00003901 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003902
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003903 # Keep a separate copy for the commit message, because the commit message
3904 # contains the link to the Rietveld issue, while the Rietveld message contains
3905 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003906 # Keep a separate copy for the commit message.
3907 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00003908 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003909
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003910 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00003911 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00003912 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00003913 # after it. Add a period on a new line to circumvent this. Also add a space
3914 # before the period to make sure that Gitiles continues to correctly resolve
3915 # the URL.
3916 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003917 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003918 commit_desc.append_footer('Patch from %s.' % options.contributor)
3919
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00003920 print('Description:')
3921 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003922
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003923 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003924 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00003925 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003926
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003927 # We want to squash all this branch's commits into one commit with the proper
3928 # description. We do this by doing a "reset --soft" to the base branch (which
3929 # keeps the working copy the same), then dcommitting that. If origin/master
3930 # has a submodule merge commit, we'll also need to cherry-pick the squashed
3931 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003932 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003933 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
3934 # Delete the branches if they exist.
3935 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
3936 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
3937 result = RunGitWithCode(showref_cmd)
3938 if result[0] == 0:
3939 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003940
3941 # We might be in a directory that's present in this branch but not in the
3942 # trunk. Move up to the top of the tree so that git commands that expect a
3943 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003944 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003945 if rel_base_path:
3946 os.chdir(rel_base_path)
3947
3948 # Stuff our change into the merge branch.
3949 # We wrap in a try...finally block so if anything goes wrong,
3950 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003951 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003952 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003953 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003954 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003955 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00003956 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003957 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003958 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003959 RunGit(
3960 [
3961 'commit', '--author', options.contributor,
3962 '-m', commit_desc.description,
3963 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003964 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003965 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003966 if base_has_submodules:
3967 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
3968 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
3969 RunGit(['checkout', CHERRY_PICK_BRANCH])
3970 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003971 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003972 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003973 mirror = settings.GetGitMirror(remote)
3974 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003975 pending_prefix = settings.GetPendingRefPrefix()
3976 if not pending_prefix or branch.startswith(pending_prefix):
3977 # If not using refs/pending/heads/* at all, or target ref is already set
3978 # to pending, then push to the target ref directly.
3979 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003980 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003981 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003982 else:
3983 # Cherry-pick the change on top of pending ref and then push it.
3984 assert branch.startswith('refs/'), branch
3985 assert pending_prefix[-1] == '/', pending_prefix
3986 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003987 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003988 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003989 if retcode == 0:
3990 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003991 else:
3992 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003993 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003994 'svn', 'dcommit',
3995 '-C%s' % options.similarity,
3996 '--no-rebase', '--rmdir',
3997 ]
3998 if settings.GetForceHttpsCommitUrl():
3999 # Allow forcing https commit URLs for some projects that don't allow
4000 # committing to http URLs (like Google Code).
4001 remote_url = cl.GetGitSvnRemoteUrl()
4002 if urlparse.urlparse(remote_url).scheme == 'http':
4003 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004004 cmd_args.append('--commit-url=%s' % remote_url)
4005 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004006 if 'Committed r' in output:
4007 revision = re.match(
4008 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4009 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004010 finally:
4011 # And then swap back to the original branch and clean up.
4012 RunGit(['checkout', '-q', cl.GetBranch()])
4013 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004014 if base_has_submodules:
4015 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004016
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004017 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004018 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004019 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004020
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004021 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004022 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004023 try:
4024 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4025 # We set pushed_to_pending to False, since it made it all the way to the
4026 # real ref.
4027 pushed_to_pending = False
4028 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004029 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004030
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004031 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004032 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004033 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004034 if not to_pending:
4035 if viewvc_url and revision:
4036 change_desc.append_footer(
4037 'Committed: %s%s' % (viewvc_url, revision))
4038 elif revision:
4039 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004040 print('Closing issue '
4041 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004042 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004043 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004044 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004045 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004046 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004047 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004048 if options.bypass_hooks:
4049 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4050 else:
4051 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004052 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004053 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004054
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004055 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004056 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004057 print('The commit is in the pending queue (%s).' % pending_ref)
4058 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4059 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004060
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004061 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4062 if os.path.isfile(hook):
4063 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004064
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004065 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004066
4067
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004068def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004069 print()
4070 print('Waiting for commit to be landed on %s...' % real_ref)
4071 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004072 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4073 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004074 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004075
4076 loop = 0
4077 while True:
4078 sys.stdout.write('fetching (%d)... \r' % loop)
4079 sys.stdout.flush()
4080 loop += 1
4081
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004082 if mirror:
4083 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004084 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4085 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4086 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4087 for commit in commits.splitlines():
4088 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004089 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004090 return commit
4091
4092 current_rev = to_rev
4093
4094
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004095def PushToGitPending(remote, pending_ref, upstream_ref):
4096 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4097
4098 Returns:
4099 (retcode of last operation, output log of last operation).
4100 """
4101 assert pending_ref.startswith('refs/'), pending_ref
4102 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4103 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4104 code = 0
4105 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004106 max_attempts = 3
4107 attempts_left = max_attempts
4108 while attempts_left:
4109 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004110 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004111 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004112
4113 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004114 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004115 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004116 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004117 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004118 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004119 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004120 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004121 continue
4122
4123 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004124 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004125 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004126 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004127 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004128 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4129 'the following files have merge conflicts:' % pending_ref)
4130 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4131 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004132 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004133 return code, out
4134
4135 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004136 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004137 code, out = RunGitWithCode(
4138 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4139 if code == 0:
4140 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004141 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004142 return code, out
4143
vapiera7fbd5a2016-06-16 09:17:49 -07004144 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004145 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004146 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004147 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004148 print('Fatal push error. Make sure your .netrc credentials and git '
4149 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004150 return code, out
4151
vapiera7fbd5a2016-06-16 09:17:49 -07004152 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004153 return code, out
4154
4155
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004156def IsFatalPushFailure(push_stdout):
4157 """True if retrying push won't help."""
4158 return '(prohibited by Gerrit)' in push_stdout
4159
4160
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004161@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004162def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004163 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004164 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004165 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004166 # If it looks like previous commits were mirrored with git-svn.
4167 message = """This repository appears to be a git-svn mirror, but no
4168upstream SVN master is set. You probably need to run 'git auto-svn' once."""
4169 else:
4170 message = """This doesn't appear to be an SVN repository.
4171If your project has a true, writeable git repository, you probably want to run
4172'git cl land' instead.
4173If your project has a git mirror of an upstream SVN master, you probably need
4174to run 'git svn init'.
4175
4176Using the wrong command might cause your commit to appear to succeed, and the
4177review to be closed, without actually landing upstream. If you choose to
4178proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004179 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004180 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004181 return SendUpstream(parser, args, 'dcommit')
4182
4183
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004184@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004185def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004186 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004187 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004188 print('This appears to be an SVN repository.')
4189 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004190 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004191 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004192 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004193
4194
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004195@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004196def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004197 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004198 parser.add_option('-b', dest='newbranch',
4199 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004200 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004201 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004202 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4203 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004204 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004205 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004206 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004207 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004208 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004209 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004210
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004211
4212 group = optparse.OptionGroup(
4213 parser,
4214 'Options for continuing work on the current issue uploaded from a '
4215 'different clone (e.g. different machine). Must be used independently '
4216 'from the other options. No issue number should be specified, and the '
4217 'branch must have an issue number associated with it')
4218 group.add_option('--reapply', action='store_true', dest='reapply',
4219 help='Reset the branch and reapply the issue.\n'
4220 'CAUTION: This will undo any local changes in this '
4221 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004222
4223 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004224 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004225 parser.add_option_group(group)
4226
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004227 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004228 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004229 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004230 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004231 auth_config = auth.extract_auth_config_from_options(options)
4232
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004233
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004234 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004235 if options.newbranch:
4236 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004237 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004238 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004239
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004240 cl = Changelist(auth_config=auth_config,
4241 codereview=options.forced_codereview)
4242 if not cl.GetIssue():
4243 parser.error('current branch must have an associated issue')
4244
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004245 upstream = cl.GetUpstreamBranch()
4246 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004247 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004248
4249 RunGit(['reset', '--hard', upstream])
4250 if options.pull:
4251 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004252
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004253 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4254 options.directory)
4255
4256 if len(args) != 1 or not args[0]:
4257 parser.error('Must specify issue number or url')
4258
4259 # We don't want uncommitted changes mixed up with the patch.
4260 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004261 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004262
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004263 if options.newbranch:
4264 if options.force:
4265 RunGit(['branch', '-D', options.newbranch],
4266 stderr=subprocess2.PIPE, error_ok=True)
4267 RunGit(['new-branch', options.newbranch])
4268
4269 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4270
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004271 if cl.IsGerrit():
4272 if options.reject:
4273 parser.error('--reject is not supported with Gerrit codereview.')
4274 if options.nocommit:
4275 parser.error('--nocommit is not supported with Gerrit codereview.')
4276 if options.directory:
4277 parser.error('--directory is not supported with Gerrit codereview.')
4278
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004279 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004280 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004281
4282
4283def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004284 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004285 # Provide a wrapper for git svn rebase to help avoid accidental
4286 # git svn dcommit.
4287 # It's the only command that doesn't use parser at all since we just defer
4288 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004289
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004290 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004291
4292
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004293def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004294 """Fetches the tree status and returns either 'open', 'closed',
4295 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004296 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004297 if url:
4298 status = urllib2.urlopen(url).read().lower()
4299 if status.find('closed') != -1 or status == '0':
4300 return 'closed'
4301 elif status.find('open') != -1 or status == '1':
4302 return 'open'
4303 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004304 return 'unset'
4305
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004306
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004307def GetTreeStatusReason():
4308 """Fetches the tree status from a json url and returns the message
4309 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004310 url = settings.GetTreeStatusUrl()
4311 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004312 connection = urllib2.urlopen(json_url)
4313 status = json.loads(connection.read())
4314 connection.close()
4315 return status['message']
4316
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004317
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004318def GetBuilderMaster(bot_list):
4319 """For a given builder, fetch the master from AE if available."""
4320 map_url = 'https://builders-map.appspot.com/'
4321 try:
4322 master_map = json.load(urllib2.urlopen(map_url))
4323 except urllib2.URLError as e:
4324 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4325 (map_url, e))
4326 except ValueError as e:
4327 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4328 if not master_map:
4329 return None, 'Failed to build master map.'
4330
4331 result_master = ''
4332 for bot in bot_list:
4333 builder = bot.split(':', 1)[0]
4334 master_list = master_map.get(builder, [])
4335 if not master_list:
4336 return None, ('No matching master for builder %s.' % builder)
4337 elif len(master_list) > 1:
4338 return None, ('The builder name %s exists in multiple masters %s.' %
4339 (builder, master_list))
4340 else:
4341 cur_master = master_list[0]
4342 if not result_master:
4343 result_master = cur_master
4344 elif result_master != cur_master:
4345 return None, 'The builders do not belong to the same master.'
4346 return result_master, None
4347
4348
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004349def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004350 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004351 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004352 status = GetTreeStatus()
4353 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004354 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004355 return 2
4356
vapiera7fbd5a2016-06-16 09:17:49 -07004357 print('The tree is %s' % status)
4358 print()
4359 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004360 if status != 'open':
4361 return 1
4362 return 0
4363
4364
maruel@chromium.org15192402012-09-06 12:38:29 +00004365def CMDtry(parser, args):
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004366 """Triggers try jobs through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00004367 group = optparse.OptionGroup(parser, "Try job options")
4368 group.add_option(
4369 "-b", "--bot", action="append",
4370 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
4371 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004372 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00004373 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004374 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00004375 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004376 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00004377 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004378 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004379 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004380 "-r", "--revision",
4381 help="Revision to use for the try job; default: the "
4382 "revision will be determined by the try server; see "
4383 "its waterfall for more info")
4384 group.add_option(
4385 "-c", "--clobber", action="store_true", default=False,
4386 help="Force a clobber before building; e.g. don't do an "
4387 "incremental build")
4388 group.add_option(
4389 "--project",
4390 help="Override which project to use. Projects are defined "
4391 "server-side to define what default bot set to use")
4392 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004393 "-p", "--property", dest="properties", action="append", default=[],
4394 help="Specify generic properties in the form -p key1=value1 -p "
4395 "key2=value2 etc (buildbucket only). The value will be treated as "
4396 "json if decodable, or as string otherwise.")
4397 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004398 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004399 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004400 "--use-rietveld", action="store_true", default=False,
4401 help="Use Rietveld to trigger try jobs.")
4402 group.add_option(
4403 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4404 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004405 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004406 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004407 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004408 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004409
machenbach@chromium.org45453142015-09-15 08:45:22 +00004410 if options.use_rietveld and options.properties:
4411 parser.error('Properties can only be specified with buildbucket')
4412
4413 # Make sure that all properties are prop=value pairs.
4414 bad_params = [x for x in options.properties if '=' not in x]
4415 if bad_params:
4416 parser.error('Got properties with missing "=": %s' % bad_params)
4417
maruel@chromium.org15192402012-09-06 12:38:29 +00004418 if args:
4419 parser.error('Unknown arguments: %s' % args)
4420
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004421 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004422 if not cl.GetIssue():
4423 parser.error('Need to upload first')
4424
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004425 if cl.IsGerrit():
4426 parser.error(
4427 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4428 'If your project has Commit Queue, dry run is a workaround:\n'
4429 ' git cl set-commit --dry-run')
4430 # Code below assumes Rietveld issue.
4431 # TODO(tandrii): actually implement for Gerrit http://crbug.com/599931.
4432
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004433 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004434 if props.get('closed'):
4435 parser.error('Cannot send tryjobs for a closed CL')
4436
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004437 if props.get('private'):
4438 parser.error('Cannot use trybots with private issue')
4439
maruel@chromium.org15192402012-09-06 12:38:29 +00004440 if not options.name:
4441 options.name = cl.GetBranch()
4442
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004443 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004444 options.master, err_msg = GetBuilderMaster(options.bot)
4445 if err_msg:
4446 parser.error('Tryserver master cannot be found because: %s\n'
4447 'Please manually specify the tryserver master'
4448 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004449
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004450 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004451 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004452 if not options.bot:
4453 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004454
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004455 # Get try masters from PRESUBMIT.py files.
4456 masters = presubmit_support.DoGetTryMasters(
4457 change,
4458 change.LocalPaths(),
4459 settings.GetRoot(),
4460 None,
4461 None,
4462 options.verbose,
4463 sys.stdout)
4464 if masters:
4465 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004466
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004467 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4468 options.bot = presubmit_support.DoGetTrySlaves(
4469 change,
4470 change.LocalPaths(),
4471 settings.GetRoot(),
4472 None,
4473 None,
4474 options.verbose,
4475 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004476
4477 if not options.bot:
4478 # Get try masters from cq.cfg if any.
4479 # TODO(tandrii): some (but very few) projects store cq.cfg in different
4480 # location.
4481 cq_cfg = os.path.join(change.RepositoryRoot(),
4482 'infra', 'config', 'cq.cfg')
4483 if os.path.exists(cq_cfg):
4484 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00004485 cq_masters = commit_queue.get_master_builder_map(
4486 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004487 for master, builders in cq_masters.iteritems():
4488 for builder in builders:
4489 # Skip presubmit builders, because these will fail without LGTM.
machenbach@chromium.org2403e802016-04-29 12:34:42 +00004490 masters.setdefault(master, {})[builder] = ['defaulttests']
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004491 if masters:
tandriib93dd2b2016-06-07 08:03:08 -07004492 print('Loaded default bots from CQ config (%s)' % cq_cfg)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004493 return masters
tandriib93dd2b2016-06-07 08:03:08 -07004494 else:
4495 print('CQ config exists (%s) but has no try bots listed' % cq_cfg)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004496
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004497 if not options.bot:
4498 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00004499
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004500 builders_and_tests = {}
4501 # TODO(machenbach): The old style command-line options don't support
4502 # multiple try masters yet.
4503 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4504 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4505
4506 for bot in old_style:
4507 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004508 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004509 elif ',' in bot:
4510 parser.error('Specify one bot per --bot flag')
4511 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004512 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004513
4514 for bot, tests in new_style:
4515 builders_and_tests.setdefault(bot, []).extend(tests)
4516
4517 # Return a master map with one master to be backwards compatible. The
4518 # master name defaults to an empty string, which will cause the master
4519 # not to be set on rietveld (deprecated).
4520 return {options.master: builders_and_tests}
4521
4522 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004523
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004524 for builders in masters.itervalues():
4525 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004526 print('ERROR You are trying to send a job to a triggered bot. This type '
4527 'of bot requires an\ninitial job from a parent (usually a builder).'
4528 ' Instead send your job to the parent.\n'
4529 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004530 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004531
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004532 patchset = cl.GetMostRecentPatchset()
4533 if patchset and patchset != cl.GetPatchset():
4534 print(
4535 '\nWARNING Mismatch between local config and server. Did a previous '
4536 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4537 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004538 if options.luci:
4539 trigger_luci_job(cl, masters, options)
4540 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004541 try:
4542 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4543 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004544 print('ERROR: %s' % ex)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004545 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004546 except Exception as e:
4547 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
vapiera7fbd5a2016-06-16 09:17:49 -07004548 print('ERROR: Exception when trying to trigger tryjobs: %s\n%s' %
4549 (e, stacktrace))
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004550 return 1
4551 else:
4552 try:
4553 cl.RpcServer().trigger_distributed_try_jobs(
4554 cl.GetIssue(), patchset, options.name, options.clobber,
4555 options.revision, masters)
4556 except urllib2.HTTPError as e:
4557 if e.code == 404:
4558 print('404 from rietveld; '
4559 'did you mean to use "git try" instead of "git cl try"?')
4560 return 1
4561 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004562
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004563 for (master, builders) in sorted(masters.iteritems()):
4564 if master:
vapiera7fbd5a2016-06-16 09:17:49 -07004565 print('Master: %s' % master)
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004566 length = max(len(builder) for builder in builders)
4567 for builder in sorted(builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004568 print(' %*s: %s' % (length, builder, ','.join(builders[builder])))
maruel@chromium.org15192402012-09-06 12:38:29 +00004569 return 0
4570
4571
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004572def CMDtry_results(parser, args):
4573 group = optparse.OptionGroup(parser, "Try job results options")
4574 group.add_option(
4575 "-p", "--patchset", type=int, help="patchset number if not current.")
4576 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004577 "--print-master", action='store_true', help="print master name as well.")
4578 group.add_option(
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004579 "--color", action='store_true', default=setup_color.IS_TTY,
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004580 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004581 group.add_option(
4582 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4583 help="Host of buildbucket. The default host is %default.")
4584 parser.add_option_group(group)
4585 auth.add_auth_options(parser)
4586 options, args = parser.parse_args(args)
4587 if args:
4588 parser.error('Unrecognized args: %s' % ' '.join(args))
4589
4590 auth_config = auth.extract_auth_config_from_options(options)
4591 cl = Changelist(auth_config=auth_config)
4592 if not cl.GetIssue():
4593 parser.error('Need to upload first')
4594
4595 if not options.patchset:
4596 options.patchset = cl.GetMostRecentPatchset()
4597 if options.patchset and options.patchset != cl.GetPatchset():
4598 print(
4599 '\nWARNING Mismatch between local config and server. Did a previous '
4600 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4601 'Continuing using\npatchset %s.\n' % options.patchset)
4602 try:
4603 jobs = fetch_try_jobs(auth_config, cl, options)
4604 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004605 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004606 return 1
4607 except Exception as e:
4608 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
vapiera7fbd5a2016-06-16 09:17:49 -07004609 print('ERROR: Exception when trying to fetch tryjobs: %s\n%s' %
4610 (e, stacktrace))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004611 return 1
4612 print_tryjobs(options, jobs)
4613 return 0
4614
4615
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004616@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004617def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004618 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004619 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004620 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004621 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004622
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004623 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004624 if args:
4625 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004626 branch = cl.GetBranch()
4627 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004628 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004629 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004630
4631 # Clear configured merge-base, if there is one.
4632 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004633 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004634 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004635 return 0
4636
4637
thestig@chromium.org00858c82013-12-02 23:08:03 +00004638def CMDweb(parser, args):
4639 """Opens the current CL in the web browser."""
4640 _, args = parser.parse_args(args)
4641 if args:
4642 parser.error('Unrecognized args: %s' % ' '.join(args))
4643
4644 issue_url = Changelist().GetIssueURL()
4645 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004646 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004647 return 1
4648
4649 webbrowser.open(issue_url)
4650 return 0
4651
4652
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004653def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004654 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004655 parser.add_option('-d', '--dry-run', action='store_true',
4656 help='trigger in dry run mode')
4657 parser.add_option('-c', '--clear', action='store_true',
4658 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004659 auth.add_auth_options(parser)
4660 options, args = parser.parse_args(args)
4661 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004662 if args:
4663 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004664 if options.dry_run and options.clear:
4665 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4666
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004667 cl = Changelist(auth_config=auth_config)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004668 if options.clear:
4669 state = _CQState.CLEAR
4670 elif options.dry_run:
4671 state = _CQState.DRY_RUN
4672 else:
4673 state = _CQState.COMMIT
4674 if not cl.GetIssue():
4675 parser.error('Must upload the issue first')
4676 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004677 return 0
4678
4679
groby@chromium.org411034a2013-02-26 15:12:01 +00004680def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004681 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004682 auth.add_auth_options(parser)
4683 options, args = parser.parse_args(args)
4684 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004685 if args:
4686 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004687 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00004688 # Ensure there actually is an issue to close.
4689 cl.GetDescription()
4690 cl.CloseIssue()
4691 return 0
4692
4693
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004694def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004695 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004696 auth.add_auth_options(parser)
4697 options, args = parser.parse_args(args)
4698 auth_config = auth.extract_auth_config_from_options(options)
4699 if args:
4700 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004701
4702 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004703 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004704 # Staged changes would be committed along with the patch from last
4705 # upload, hence counted toward the "last upload" side in the final
4706 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004707 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004708 return 1
4709
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004710 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004711 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004712 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004713 if not issue:
4714 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004715 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004716 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004717
4718 # Create a new branch based on the merge-base
4719 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004720 # Clear cached branch in cl object, to avoid overwriting original CL branch
4721 # properties.
4722 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004723 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004724 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004725 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004726 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004727 return rtn
4728
wychen@chromium.org06928532015-02-03 02:11:29 +00004729 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004730 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00004731 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004732 finally:
4733 RunGit(['checkout', '-q', branch])
4734 RunGit(['branch', '-D', TMP_BRANCH])
4735
4736 return 0
4737
4738
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004739def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004740 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004741 parser.add_option(
4742 '--no-color',
4743 action='store_true',
4744 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004745 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004746 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004747 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004748
4749 author = RunGit(['config', 'user.email']).strip() or None
4750
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004751 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004752
4753 if args:
4754 if len(args) > 1:
4755 parser.error('Unknown args')
4756 base_branch = args[0]
4757 else:
4758 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004759 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004760
4761 change = cl.GetChange(base_branch, None)
4762 return owners_finder.OwnersFinder(
4763 [f.LocalPath() for f in
4764 cl.GetChange(base_branch, None).AffectedFiles()],
4765 change.RepositoryRoot(), author,
4766 fopen=file, os_path=os.path, glob=glob.glob,
4767 disable_color=options.no_color).run()
4768
4769
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004770def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004771 """Generates a diff command."""
4772 # Generate diff for the current branch's changes.
4773 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4774 upstream_commit, '--' ]
4775
4776 if args:
4777 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004778 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004779 diff_cmd.append(arg)
4780 else:
4781 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004782
4783 return diff_cmd
4784
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004785def MatchingFileType(file_name, extensions):
4786 """Returns true if the file name ends with one of the given extensions."""
4787 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004788
enne@chromium.org555cfe42014-01-29 18:21:39 +00004789@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004790def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004791 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00004792 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004793 GN_EXTS = ['.gn', '.gni']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004794 parser.add_option('--full', action='store_true',
4795 help='Reformat the full content of all touched files')
4796 parser.add_option('--dry-run', action='store_true',
4797 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004798 parser.add_option('--python', action='store_true',
4799 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004800 parser.add_option('--diff', action='store_true',
4801 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004802 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004803
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004804 # git diff generates paths against the root of the repository. Change
4805 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004806 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004807 if rel_base_path:
4808 os.chdir(rel_base_path)
4809
digit@chromium.org29e47272013-05-17 17:01:46 +00004810 # Grab the merge-base commit, i.e. the upstream commit of the current
4811 # branch when it was created or the last time it was rebased. This is
4812 # to cover the case where the user may have called "git fetch origin",
4813 # moving the origin branch to a newer commit, but hasn't rebased yet.
4814 upstream_commit = None
4815 cl = Changelist()
4816 upstream_branch = cl.GetUpstreamBranch()
4817 if upstream_branch:
4818 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4819 upstream_commit = upstream_commit.strip()
4820
4821 if not upstream_commit:
4822 DieWithError('Could not find base commit for this branch. '
4823 'Are you in detached state?')
4824
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004825 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4826 diff_output = RunGit(changed_files_cmd)
4827 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004828 # Filter out files deleted by this CL
4829 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004830
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004831 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4832 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4833 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004834 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004835
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004836 top_dir = os.path.normpath(
4837 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
4838
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004839 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4840 # formatted. This is used to block during the presubmit.
4841 return_value = 0
4842
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004843 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004844 # Locate the clang-format binary in the checkout
4845 try:
4846 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07004847 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004848 DieWithError(e)
4849
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004850 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004851 cmd = [clang_format_tool]
4852 if not opts.dry_run and not opts.diff:
4853 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004854 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004855 if opts.diff:
4856 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004857 else:
4858 env = os.environ.copy()
4859 env['PATH'] = str(os.path.dirname(clang_format_tool))
4860 try:
4861 script = clang_format.FindClangFormatScriptInChromiumTree(
4862 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07004863 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004864 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004865
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004866 cmd = [sys.executable, script, '-p0']
4867 if not opts.dry_run and not opts.diff:
4868 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004869
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004870 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
4871 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004872
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004873 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
4874 if opts.diff:
4875 sys.stdout.write(stdout)
4876 if opts.dry_run and len(stdout) > 0:
4877 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004878
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004879 # Similar code to above, but using yapf on .py files rather than clang-format
4880 # on C/C++ files
4881 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004882 yapf_tool = gclient_utils.FindExecutable('yapf')
4883 if yapf_tool is None:
4884 DieWithError('yapf not found in PATH')
4885
4886 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004887 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004888 cmd = [yapf_tool]
4889 if not opts.dry_run and not opts.diff:
4890 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004891 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004892 if opts.diff:
4893 sys.stdout.write(stdout)
4894 else:
4895 # TODO(sbc): yapf --lines mode still has some issues.
4896 # https://github.com/google/yapf/issues/154
4897 DieWithError('--python currently only works with --full')
4898
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004899 # Dart's formatter does not have the nice property of only operating on
4900 # modified chunks, so hard code full.
4901 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004902 try:
4903 command = [dart_format.FindDartFmtToolInChromiumTree()]
4904 if not opts.dry_run and not opts.diff:
4905 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004906 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004907
ppi@chromium.org6593d932016-03-03 15:41:15 +00004908 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004909 if opts.dry_run and stdout:
4910 return_value = 2
4911 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07004912 print('Warning: Unable to check Dart code formatting. Dart SDK not '
4913 'found in this checkout. Files in other languages are still '
4914 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004915
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004916 # Format GN build files. Always run on full build files for canonical form.
4917 if gn_diff_files:
4918 cmd = ['gn', 'format']
4919 if not opts.dry_run and not opts.diff:
4920 cmd.append('--in-place')
4921 for gn_diff_file in gn_diff_files:
bsep@chromium.org627d9002016-04-29 00:00:52 +00004922 stdout = RunCommand(cmd + [gn_diff_file],
4923 shell=sys.platform == 'win32',
4924 cwd=top_dir)
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004925 if opts.diff:
4926 sys.stdout.write(stdout)
4927
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004928 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004929
4930
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004931@subcommand.usage('<codereview url or issue id>')
4932def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004933 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004934 _, args = parser.parse_args(args)
4935
4936 if len(args) != 1:
4937 parser.print_help()
4938 return 1
4939
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004940 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00004941 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004942 parser.print_help()
4943 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00004944 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004945
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004946 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00004947 output = RunGit(['config', '--local', '--get-regexp',
4948 r'branch\..*\.%s' % issueprefix],
4949 error_ok=True)
4950 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004951 if issue == target_issue:
4952 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004953
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004954 branches = []
4955 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00004956 branches.extend(find_issues(cls.IssueSettingSuffix()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004957 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07004958 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004959 return 1
4960 if len(branches) == 1:
4961 RunGit(['checkout', branches[0]])
4962 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004963 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004964 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07004965 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004966 which = raw_input('Choose by index: ')
4967 try:
4968 RunGit(['checkout', branches[int(which)]])
4969 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07004970 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004971 return 1
4972
4973 return 0
4974
4975
maruel@chromium.org29404b52014-09-08 22:58:00 +00004976def CMDlol(parser, args):
4977 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07004978 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00004979 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
4980 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
4981 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07004982 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00004983 return 0
4984
4985
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004986class OptionParser(optparse.OptionParser):
4987 """Creates the option parse and add --verbose support."""
4988 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004989 optparse.OptionParser.__init__(
4990 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004991 self.add_option(
4992 '-v', '--verbose', action='count', default=0,
4993 help='Use 2 times for more debugging info')
4994
4995 def parse_args(self, args=None, values=None):
4996 options, args = optparse.OptionParser.parse_args(self, args, values)
4997 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
4998 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
4999 return options, args
5000
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005001
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005002def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005003 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005004 print('\nYour python version %s is unsupported, please upgrade.\n' %
5005 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005006 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005007
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005008 # Reload settings.
5009 global settings
5010 settings = Settings()
5011
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005012 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005013 dispatcher = subcommand.CommandDispatcher(__name__)
5014 try:
5015 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005016 except auth.AuthenticationError as e:
5017 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005018 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005019 if e.code != 500:
5020 raise
5021 DieWithError(
5022 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5023 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005024 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005025
5026
5027if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005028 # These affect sys.stdout so do it outside of main() to simplify mocks in
5029 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005030 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005031 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005032 try:
5033 sys.exit(main(sys.argv[1:]))
5034 except KeyboardInterrupt:
5035 sys.stderr.write('interrupted\n')
5036 sys.exit(1)