blob: 6506f2923b7fe21d2c2a0a9de9b1b1495a0a7bb6 [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00008"""A git-command for integrating reviews on Rietveld and Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
vapiera7fbd5a2016-06-16 09:17:49 -070010from __future__ import print_function
11
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000012from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000013from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000014import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000015import collections
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000016import glob
sheyang@google.com6ebaf782015-05-12 19:17:54 +000017import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000018import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000020import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import optparse
22import os
23import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000024import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000026import textwrap
sheyang@google.com6ebaf782015-05-12 19:17:54 +000027import time
28import traceback
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000029import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000030import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000031import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000032import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000033import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000034import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000035
36try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000037 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000038except ImportError:
39 pass
40
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000041from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000042from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000043from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000044import auth
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +000045from luci_hacks import trigger_luci_job as luci_trigger
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000046import clang_format
tandrii@chromium.org71184c02016-01-13 15:18:44 +000047import commit_queue
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000048import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000049import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000050import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000051import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000052import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000053import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000054import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000055import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000056import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000057import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000058import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000059import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000060import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000061import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000062import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000063import watchlists
64
tandrii7400cf02016-06-21 08:48:07 -070065__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000066
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000067DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000068POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000069DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000070GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
rmistry@google.comc68112d2015-03-03 12:48:06 +000071REFS_THAT_ALIAS_TO_OTHER_REFS = {
72 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
73 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
74}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000075
thestig@chromium.org44202a22014-03-11 19:22:18 +000076# Valid extensions for files we want to lint.
77DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
78DEFAULT_LINT_IGNORE_REGEX = r"$^"
79
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000080# Shortcut since it quickly becomes redundant.
81Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000082
maruel@chromium.orgddd59412011-11-30 14:20:38 +000083# Initialized in main()
84settings = None
85
86
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000087def DieWithError(message):
vapiera7fbd5a2016-06-16 09:17:49 -070088 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000089 sys.exit(1)
90
91
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000092def GetNoGitPagerEnv():
93 env = os.environ.copy()
94 # 'cat' is a magical git string that disables pagers on all platforms.
95 env['GIT_PAGER'] = 'cat'
96 return env
97
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000098
bsep@chromium.org627d9002016-04-29 00:00:52 +000099def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000100 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000101 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000102 except subprocess2.CalledProcessError as e:
103 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000104 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000105 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000106 'Command "%s" failed.\n%s' % (
107 ' '.join(args), error_message or e.stdout or ''))
108 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000109
110
111def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000112 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000113 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000114
115
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000116def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000117 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000118 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000119 if suppress_stderr:
120 stderr = subprocess2.VOID
121 else:
122 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000123 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000124 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000125 stdout=subprocess2.PIPE,
126 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000127 return code, out[0]
128 except ValueError:
129 # When the subprocess fails, it returns None. That triggers a ValueError
130 # when trying to unpack the return value into (out, code).
131 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000132
133
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000134def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000135 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000136 return RunGitWithCode(args, suppress_stderr=True)[1]
137
138
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000139def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000140 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000141 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000142 return (version.startswith(prefix) and
143 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000144
145
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000146def BranchExists(branch):
147 """Return True if specified branch exists."""
148 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
149 suppress_stderr=True)
150 return not code
151
152
maruel@chromium.org90541732011-04-01 17:54:18 +0000153def ask_for_data(prompt):
154 try:
155 return raw_input(prompt)
156 except KeyboardInterrupt:
157 # Hide the exception.
158 sys.exit(1)
159
160
iannucci@chromium.org79540052012-10-19 23:15:26 +0000161def git_set_branch_value(key, value):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000162 branch = GetCurrentBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000163 if not branch:
164 return
165
166 cmd = ['config']
167 if isinstance(value, int):
168 cmd.append('--int')
169 git_key = 'branch.%s.%s' % (branch, key)
170 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000171
172
173def git_get_branch_default(key, default):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000174 branch = GetCurrentBranch()
iannucci@chromium.org79540052012-10-19 23:15:26 +0000175 if branch:
176 git_key = 'branch.%s.%s' % (branch, key)
177 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
178 try:
179 return int(stdout.strip())
180 except ValueError:
181 pass
182 return default
183
184
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000185def add_git_similarity(parser):
186 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000187 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000188 help='Sets the percentage that a pair of files need to match in order to'
189 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000190 parser.add_option(
191 '--find-copies', action='store_true',
192 help='Allows git to look for copies.')
193 parser.add_option(
194 '--no-find-copies', action='store_false', dest='find_copies',
195 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000196
197 old_parser_args = parser.parse_args
198 def Parse(args):
199 options, args = old_parser_args(args)
200
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000201 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000202 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000203 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000204 print('Note: Saving similarity of %d%% in git config.'
205 % options.similarity)
206 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000207
iannucci@chromium.org79540052012-10-19 23:15:26 +0000208 options.similarity = max(0, min(options.similarity, 100))
209
210 if options.find_copies is None:
211 options.find_copies = bool(
212 git_get_branch_default('git-find-copies', True))
213 else:
214 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000215
216 print('Using %d%% similarity for rename/copy detection. '
217 'Override with --similarity.' % options.similarity)
218
219 return options, args
220 parser.parse_args = Parse
221
222
machenbach@chromium.org45453142015-09-15 08:45:22 +0000223def _get_properties_from_options(options):
224 properties = dict(x.split('=', 1) for x in options.properties)
225 for key, val in properties.iteritems():
226 try:
227 properties[key] = json.loads(val)
228 except ValueError:
229 pass # If a value couldn't be evaluated, treat it as a string.
230 return properties
231
232
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000233def _prefix_master(master):
234 """Convert user-specified master name to full master name.
235
236 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
237 name, while the developers always use shortened master name
238 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
239 function does the conversion for buildbucket migration.
240 """
241 prefix = 'master.'
242 if master.startswith(prefix):
243 return master
244 return '%s%s' % (prefix, master)
245
246
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000247def _buildbucket_retry(operation_name, http, *args, **kwargs):
248 """Retries requests to buildbucket service and returns parsed json content."""
249 try_count = 0
250 while True:
251 response, content = http.request(*args, **kwargs)
252 try:
253 content_json = json.loads(content)
254 except ValueError:
255 content_json = None
256
257 # Buildbucket could return an error even if status==200.
258 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000259 error = content_json.get('error')
260 if error.get('code') == 403:
261 raise BuildbucketResponseException(
262 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000263 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000264 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000265 raise BuildbucketResponseException(msg)
266
267 if response.status == 200:
268 if not content_json:
269 raise BuildbucketResponseException(
270 'Buildbucket returns invalid json content: %s.\n'
271 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
272 content)
273 return content_json
274 if response.status < 500 or try_count >= 2:
275 raise httplib2.HttpLib2Error(content)
276
277 # status >= 500 means transient failures.
278 logging.debug('Transient errors when %s. Will retry.', operation_name)
279 time.sleep(0.5 + 1.5*try_count)
280 try_count += 1
281 assert False, 'unreachable'
282
283
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000284def trigger_luci_job(changelist, masters, options):
285 """Send a job to run on LUCI."""
286 issue_props = changelist.GetIssueProperties()
287 issue = changelist.GetIssue()
288 patchset = changelist.GetMostRecentPatchset()
289 for builders_and_tests in sorted(masters.itervalues()):
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000290 # TODO(hinoka et al): add support for other properties.
291 # Currently, this completely ignores testfilter and other properties.
292 for builder in sorted(builders_and_tests):
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000293 luci_trigger.trigger(
294 builder, 'HEAD', issue, patchset, issue_props['project'])
295
296
machenbach@chromium.org45453142015-09-15 08:45:22 +0000297def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000298 rietveld_url = settings.GetDefaultServerUrl()
299 rietveld_host = urlparse.urlparse(rietveld_url).hostname
300 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
301 http = authenticator.authorize(httplib2.Http())
302 http.force_exception_to_status_code = True
303 issue_props = changelist.GetIssueProperties()
304 issue = changelist.GetIssue()
305 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000306 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000307
308 buildbucket_put_url = (
309 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000310 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000311 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
312 hostname=rietveld_host,
313 issue=issue,
314 patch=patchset)
315
316 batch_req_body = {'builds': []}
317 print_text = []
318 print_text.append('Tried jobs on:')
319 for master, builders_and_tests in sorted(masters.iteritems()):
320 print_text.append('Master: %s' % master)
321 bucket = _prefix_master(master)
322 for builder, tests in sorted(builders_and_tests.iteritems()):
323 print_text.append(' %s: %s' % (builder, tests))
324 parameters = {
325 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000326 'changes': [{
327 'author': {'email': issue_props['owner_email']},
328 'revision': options.revision,
329 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000330 'properties': {
331 'category': category,
332 'issue': issue,
333 'master': master,
334 'patch_project': issue_props['project'],
335 'patch_storage': 'rietveld',
336 'patchset': patchset,
337 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000338 'rietveld': rietveld_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000339 },
340 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000341 if 'presubmit' in builder.lower():
342 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000343 if tests:
344 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000345 if properties:
346 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000347 if options.clobber:
348 parameters['properties']['clobber'] = True
349 batch_req_body['builds'].append(
350 {
351 'bucket': bucket,
352 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000353 'client_operation_id': str(uuid.uuid4()),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000354 'tags': ['builder:%s' % builder,
355 'buildset:%s' % buildset,
356 'master:%s' % master,
357 'user_agent:git_cl_try']
358 }
359 )
360
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000361 _buildbucket_retry(
362 'triggering tryjobs',
363 http,
364 buildbucket_put_url,
365 'PUT',
366 body=json.dumps(batch_req_body),
367 headers={'Content-Type': 'application/json'}
368 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000369 print_text.append('To see results here, run: git cl try-results')
370 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700371 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000372
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000373
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000374def fetch_try_jobs(auth_config, changelist, options):
375 """Fetches tryjobs from buildbucket.
376
377 Returns a map from build id to build info as json dictionary.
378 """
379 rietveld_url = settings.GetDefaultServerUrl()
380 rietveld_host = urlparse.urlparse(rietveld_url).hostname
381 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
382 if authenticator.has_cached_credentials():
383 http = authenticator.authorize(httplib2.Http())
384 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700385 print('Warning: Some results might be missing because %s' %
386 # Get the message on how to login.
387 (auth.LoginRequiredError(rietveld_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000388 http = httplib2.Http()
389
390 http.force_exception_to_status_code = True
391
392 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
393 hostname=rietveld_host,
394 issue=changelist.GetIssue(),
395 patch=options.patchset)
396 params = {'tag': 'buildset:%s' % buildset}
397
398 builds = {}
399 while True:
400 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
401 hostname=options.buildbucket_host,
402 params=urllib.urlencode(params))
403 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
404 for build in content.get('builds', []):
405 builds[build['id']] = build
406 if 'next_cursor' in content:
407 params['start_cursor'] = content['next_cursor']
408 else:
409 break
410 return builds
411
412
413def print_tryjobs(options, builds):
414 """Prints nicely result of fetch_try_jobs."""
415 if not builds:
vapiera7fbd5a2016-06-16 09:17:49 -0700416 print('No tryjobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000417 return
418
419 # Make a copy, because we'll be modifying builds dictionary.
420 builds = builds.copy()
421 builder_names_cache = {}
422
423 def get_builder(b):
424 try:
425 return builder_names_cache[b['id']]
426 except KeyError:
427 try:
428 parameters = json.loads(b['parameters_json'])
429 name = parameters['builder_name']
430 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700431 print('WARNING: failed to get builder name for build %s: %s' % (
432 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000433 name = None
434 builder_names_cache[b['id']] = name
435 return name
436
437 def get_bucket(b):
438 bucket = b['bucket']
439 if bucket.startswith('master.'):
440 return bucket[len('master.'):]
441 return bucket
442
443 if options.print_master:
444 name_fmt = '%%-%ds %%-%ds' % (
445 max(len(str(get_bucket(b))) for b in builds.itervalues()),
446 max(len(str(get_builder(b))) for b in builds.itervalues()))
447 def get_name(b):
448 return name_fmt % (get_bucket(b), get_builder(b))
449 else:
450 name_fmt = '%%-%ds' % (
451 max(len(str(get_builder(b))) for b in builds.itervalues()))
452 def get_name(b):
453 return name_fmt % get_builder(b)
454
455 def sort_key(b):
456 return b['status'], b.get('result'), get_name(b), b.get('url')
457
458 def pop(title, f, color=None, **kwargs):
459 """Pop matching builds from `builds` dict and print them."""
460
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000461 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000462 colorize = str
463 else:
464 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
465
466 result = []
467 for b in builds.values():
468 if all(b.get(k) == v for k, v in kwargs.iteritems()):
469 builds.pop(b['id'])
470 result.append(b)
471 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700472 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000473 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700474 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000475
476 total = len(builds)
477 pop(status='COMPLETED', result='SUCCESS',
478 title='Successes:', color=Fore.GREEN,
479 f=lambda b: (get_name(b), b.get('url')))
480 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
481 title='Infra Failures:', color=Fore.MAGENTA,
482 f=lambda b: (get_name(b), b.get('url')))
483 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
484 title='Failures:', color=Fore.RED,
485 f=lambda b: (get_name(b), b.get('url')))
486 pop(status='COMPLETED', result='CANCELED',
487 title='Canceled:', color=Fore.MAGENTA,
488 f=lambda b: (get_name(b),))
489 pop(status='COMPLETED', result='FAILURE',
490 failure_reason='INVALID_BUILD_DEFINITION',
491 title='Wrong master/builder name:', color=Fore.MAGENTA,
492 f=lambda b: (get_name(b),))
493 pop(status='COMPLETED', result='FAILURE',
494 title='Other failures:',
495 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
496 pop(status='COMPLETED',
497 title='Other finished:',
498 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
499 pop(status='STARTED',
500 title='Started:', color=Fore.YELLOW,
501 f=lambda b: (get_name(b), b.get('url')))
502 pop(status='SCHEDULED',
503 title='Scheduled:',
504 f=lambda b: (get_name(b), 'id=%s' % b['id']))
505 # The last section is just in case buildbucket API changes OR there is a bug.
506 pop(title='Other:',
507 f=lambda b: (get_name(b), 'id=%s' % b['id']))
508 assert len(builds) == 0
vapiera7fbd5a2016-06-16 09:17:49 -0700509 print('Total: %d tryjobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000510
511
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000512def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
513 """Return the corresponding git ref if |base_url| together with |glob_spec|
514 matches the full |url|.
515
516 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
517 """
518 fetch_suburl, as_ref = glob_spec.split(':')
519 if allow_wildcards:
520 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
521 if glob_match:
522 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
523 # "branches/{472,597,648}/src:refs/remotes/svn/*".
524 branch_re = re.escape(base_url)
525 if glob_match.group(1):
526 branch_re += '/' + re.escape(glob_match.group(1))
527 wildcard = glob_match.group(2)
528 if wildcard == '*':
529 branch_re += '([^/]*)'
530 else:
531 # Escape and replace surrounding braces with parentheses and commas
532 # with pipe symbols.
533 wildcard = re.escape(wildcard)
534 wildcard = re.sub('^\\\\{', '(', wildcard)
535 wildcard = re.sub('\\\\,', '|', wildcard)
536 wildcard = re.sub('\\\\}$', ')', wildcard)
537 branch_re += wildcard
538 if glob_match.group(3):
539 branch_re += re.escape(glob_match.group(3))
540 match = re.match(branch_re, url)
541 if match:
542 return re.sub('\*$', match.group(1), as_ref)
543
544 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
545 if fetch_suburl:
546 full_url = base_url + '/' + fetch_suburl
547 else:
548 full_url = base_url
549 if full_url == url:
550 return as_ref
551 return None
552
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000553
iannucci@chromium.org79540052012-10-19 23:15:26 +0000554def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000555 """Prints statistics about the change to the user."""
556 # --no-ext-diff is broken in some versions of Git, so try to work around
557 # this by overriding the environment (but there is still a problem if the
558 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000559 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000560 if 'GIT_EXTERNAL_DIFF' in env:
561 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000562
563 if find_copies:
564 similarity_options = ['--find-copies-harder', '-l100000',
565 '-C%s' % similarity]
566 else:
567 similarity_options = ['-M%s' % similarity]
568
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000569 try:
570 stdout = sys.stdout.fileno()
571 except AttributeError:
572 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000573 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000574 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000575 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000576 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000577
578
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000579class BuildbucketResponseException(Exception):
580 pass
581
582
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000583class Settings(object):
584 def __init__(self):
585 self.default_server = None
586 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000587 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000588 self.is_git_svn = None
589 self.svn_branch = None
590 self.tree_status_url = None
591 self.viewvc_url = None
592 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000593 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000594 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000595 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000596 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000597 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000598 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000599 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000600
601 def LazyUpdateIfNeeded(self):
602 """Updates the settings from a codereview.settings file, if available."""
603 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000604 # The only value that actually changes the behavior is
605 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000606 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000607 error_ok=True
608 ).strip().lower()
609
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000610 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000611 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000612 LoadCodereviewSettingsFromFile(cr_settings_file)
613 self.updated = True
614
615 def GetDefaultServerUrl(self, error_ok=False):
616 if not self.default_server:
617 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000618 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000619 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000620 if error_ok:
621 return self.default_server
622 if not self.default_server:
623 error_message = ('Could not find settings file. You must configure '
624 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000625 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000626 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000627 return self.default_server
628
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000629 @staticmethod
630 def GetRelativeRoot():
631 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000632
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000633 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000634 if self.root is None:
635 self.root = os.path.abspath(self.GetRelativeRoot())
636 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000637
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000638 def GetGitMirror(self, remote='origin'):
639 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000640 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000641 if not os.path.isdir(local_url):
642 return None
643 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
644 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
645 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
646 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
647 if mirror.exists():
648 return mirror
649 return None
650
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000651 def GetIsGitSvn(self):
652 """Return true if this repo looks like it's using git-svn."""
653 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000654 if self.GetPendingRefPrefix():
655 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
656 self.is_git_svn = False
657 else:
658 # If you have any "svn-remote.*" config keys, we think you're using svn.
659 self.is_git_svn = RunGitWithCode(
660 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000661 return self.is_git_svn
662
663 def GetSVNBranch(self):
664 if self.svn_branch is None:
665 if not self.GetIsGitSvn():
666 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
667
668 # Try to figure out which remote branch we're based on.
669 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000670 # 1) iterate through our branch history and find the svn URL.
671 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000672
673 # regexp matching the git-svn line that contains the URL.
674 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
675
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000676 # We don't want to go through all of history, so read a line from the
677 # pipe at a time.
678 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000679 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000680 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
681 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000682 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000683 for line in proc.stdout:
684 match = git_svn_re.match(line)
685 if match:
686 url = match.group(1)
687 proc.stdout.close() # Cut pipe.
688 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000689
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000690 if url:
691 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
692 remotes = RunGit(['config', '--get-regexp',
693 r'^svn-remote\..*\.url']).splitlines()
694 for remote in remotes:
695 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000696 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000697 remote = match.group(1)
698 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000699 rewrite_root = RunGit(
700 ['config', 'svn-remote.%s.rewriteRoot' % remote],
701 error_ok=True).strip()
702 if rewrite_root:
703 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000704 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000705 ['config', 'svn-remote.%s.fetch' % remote],
706 error_ok=True).strip()
707 if fetch_spec:
708 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
709 if self.svn_branch:
710 break
711 branch_spec = RunGit(
712 ['config', 'svn-remote.%s.branches' % remote],
713 error_ok=True).strip()
714 if branch_spec:
715 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
716 if self.svn_branch:
717 break
718 tag_spec = RunGit(
719 ['config', 'svn-remote.%s.tags' % remote],
720 error_ok=True).strip()
721 if tag_spec:
722 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
723 if self.svn_branch:
724 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000725
726 if not self.svn_branch:
727 DieWithError('Can\'t guess svn branch -- try specifying it on the '
728 'command line')
729
730 return self.svn_branch
731
732 def GetTreeStatusUrl(self, error_ok=False):
733 if not self.tree_status_url:
734 error_message = ('You must configure your tree status URL by running '
735 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000736 self.tree_status_url = self._GetRietveldConfig(
737 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000738 return self.tree_status_url
739
740 def GetViewVCUrl(self):
741 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000742 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000743 return self.viewvc_url
744
rmistry@google.com90752582014-01-14 21:04:50 +0000745 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000746 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000747
rmistry@google.com78948ed2015-07-08 23:09:57 +0000748 def GetIsSkipDependencyUpload(self, branch_name):
749 """Returns true if specified branch should skip dep uploads."""
750 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
751 error_ok=True)
752
rmistry@google.com5626a922015-02-26 14:03:30 +0000753 def GetRunPostUploadHook(self):
754 run_post_upload_hook = self._GetRietveldConfig(
755 'run-post-upload-hook', error_ok=True)
756 return run_post_upload_hook == "True"
757
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000758 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000759 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000760
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000761 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000762 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000763
ukai@chromium.orge8077812012-02-03 03:41:46 +0000764 def GetIsGerrit(self):
765 """Return true if this repo is assosiated with gerrit code review system."""
766 if self.is_gerrit is None:
767 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
768 return self.is_gerrit
769
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000770 def GetSquashGerritUploads(self):
771 """Return true if uploads to Gerrit should be squashed by default."""
772 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700773 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
774 if self.squash_gerrit_uploads is None:
775 # Default is squash now (http://crbug.com/611892#c23).
776 self.squash_gerrit_uploads = not (
777 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
778 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000779 return self.squash_gerrit_uploads
780
tandriia60502f2016-06-20 02:01:53 -0700781 def GetSquashGerritUploadsOverride(self):
782 """Return True or False if codereview.settings should be overridden.
783
784 Returns None if no override has been defined.
785 """
786 # See also http://crbug.com/611892#c23
787 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
788 error_ok=True).strip()
789 if result == 'true':
790 return True
791 if result == 'false':
792 return False
793 return None
794
tandrii@chromium.org28253532016-04-14 13:46:56 +0000795 def GetGerritSkipEnsureAuthenticated(self):
796 """Return True if EnsureAuthenticated should not be done for Gerrit
797 uploads."""
798 if self.gerrit_skip_ensure_authenticated is None:
799 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000800 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000801 error_ok=True).strip() == 'true')
802 return self.gerrit_skip_ensure_authenticated
803
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000804 def GetGitEditor(self):
805 """Return the editor specified in the git config, or None if none is."""
806 if self.git_editor is None:
807 self.git_editor = self._GetConfig('core.editor', error_ok=True)
808 return self.git_editor or None
809
thestig@chromium.org44202a22014-03-11 19:22:18 +0000810 def GetLintRegex(self):
811 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
812 DEFAULT_LINT_REGEX)
813
814 def GetLintIgnoreRegex(self):
815 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
816 DEFAULT_LINT_IGNORE_REGEX)
817
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000818 def GetProject(self):
819 if not self.project:
820 self.project = self._GetRietveldConfig('project', error_ok=True)
821 return self.project
822
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000823 def GetForceHttpsCommitUrl(self):
824 if not self.force_https_commit_url:
825 self.force_https_commit_url = self._GetRietveldConfig(
826 'force-https-commit-url', error_ok=True)
827 return self.force_https_commit_url
828
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000829 def GetPendingRefPrefix(self):
830 if not self.pending_ref_prefix:
831 self.pending_ref_prefix = self._GetRietveldConfig(
832 'pending-ref-prefix', error_ok=True)
833 return self.pending_ref_prefix
834
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000835 def _GetRietveldConfig(self, param, **kwargs):
836 return self._GetConfig('rietveld.' + param, **kwargs)
837
rmistry@google.com78948ed2015-07-08 23:09:57 +0000838 def _GetBranchConfig(self, branch_name, param, **kwargs):
839 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
840
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000841 def _GetConfig(self, param, **kwargs):
842 self.LazyUpdateIfNeeded()
843 return RunGit(['config', param], **kwargs).strip()
844
845
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000846def ShortBranchName(branch):
847 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000848 return branch.replace('refs/heads/', '', 1)
849
850
851def GetCurrentBranchRef():
852 """Returns branch ref (e.g., refs/heads/master) or None."""
853 return RunGit(['symbolic-ref', 'HEAD'],
854 stderr=subprocess2.VOID, error_ok=True).strip() or None
855
856
857def GetCurrentBranch():
858 """Returns current branch or None.
859
860 For refs/heads/* branches, returns just last part. For others, full ref.
861 """
862 branchref = GetCurrentBranchRef()
863 if branchref:
864 return ShortBranchName(branchref)
865 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000866
867
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000868class _CQState(object):
869 """Enum for states of CL with respect to Commit Queue."""
870 NONE = 'none'
871 DRY_RUN = 'dry_run'
872 COMMIT = 'commit'
873
874 ALL_STATES = [NONE, DRY_RUN, COMMIT]
875
876
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000877class _ParsedIssueNumberArgument(object):
878 def __init__(self, issue=None, patchset=None, hostname=None):
879 self.issue = issue
880 self.patchset = patchset
881 self.hostname = hostname
882
883 @property
884 def valid(self):
885 return self.issue is not None
886
887
888class _RietveldParsedIssueNumberArgument(_ParsedIssueNumberArgument):
889 def __init__(self, *args, **kwargs):
890 self.patch_url = kwargs.pop('patch_url', None)
891 super(_RietveldParsedIssueNumberArgument, self).__init__(*args, **kwargs)
892
893
894def ParseIssueNumberArgument(arg):
895 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
896 fail_result = _ParsedIssueNumberArgument()
897
898 if arg.isdigit():
899 return _ParsedIssueNumberArgument(issue=int(arg))
900 if not arg.startswith('http'):
901 return fail_result
902 url = gclient_utils.UpgradeToHttps(arg)
903 try:
904 parsed_url = urlparse.urlparse(url)
905 except ValueError:
906 return fail_result
907 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
908 tmp = cls.ParseIssueURL(parsed_url)
909 if tmp is not None:
910 return tmp
911 return fail_result
912
913
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000914class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000915 """Changelist works with one changelist in local branch.
916
917 Supports two codereview backends: Rietveld or Gerrit, selected at object
918 creation.
919
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +0000920 Notes:
921 * Not safe for concurrent multi-{thread,process} use.
922 * Caches values from current branch. Therefore, re-use after branch change
923 with care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000924 """
925
926 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
927 """Create a new ChangeList instance.
928
929 If issue is given, the codereview must be given too.
930
931 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
932 Otherwise, it's decided based on current configuration of the local branch,
933 with default being 'rietveld' for backwards compatibility.
934 See _load_codereview_impl for more details.
935
936 **kwargs will be passed directly to codereview implementation.
937 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000938 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000939 global settings
940 if not settings:
941 # Happens when git_cl.py is used as a utility library.
942 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000943
944 if issue:
945 assert codereview, 'codereview must be known, if issue is known'
946
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000947 self.branchref = branchref
948 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000949 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000950 self.branch = ShortBranchName(self.branchref)
951 else:
952 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000953 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000954 self.lookedup_issue = False
955 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000956 self.has_description = False
957 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000958 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000959 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000960 self.cc = None
961 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000962 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000963
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000964 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000965 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000966 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000967 assert self._codereview_impl
968 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000969
970 def _load_codereview_impl(self, codereview=None, **kwargs):
971 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000972 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
973 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
974 self._codereview = codereview
975 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000976 return
977
978 # Automatic selection based on issue number set for a current branch.
979 # Rietveld takes precedence over Gerrit.
980 assert not self.issue
981 # Whether we find issue or not, we are doing the lookup.
982 self.lookedup_issue = True
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000983 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000984 setting = cls.IssueSetting(self.GetBranch())
985 issue = RunGit(['config', setting], error_ok=True).strip()
986 if issue:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000987 self._codereview = codereview
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000988 self._codereview_impl = cls(self, **kwargs)
989 self.issue = int(issue)
990 return
991
992 # No issue is set for this branch, so decide based on repo-wide settings.
993 return self._load_codereview_impl(
994 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
995 **kwargs)
996
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000997 def IsGerrit(self):
998 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000999
1000 def GetCCList(self):
1001 """Return the users cc'd on this CL.
1002
1003 Return is a string suitable for passing to gcl with the --cc flag.
1004 """
1005 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001006 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001007 more_cc = ','.join(self.watchers)
1008 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1009 return self.cc
1010
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001011 def GetCCListWithoutDefault(self):
1012 """Return the users cc'd on this CL excluding default ones."""
1013 if self.cc is None:
1014 self.cc = ','.join(self.watchers)
1015 return self.cc
1016
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001017 def SetWatchers(self, watchers):
1018 """Set the list of email addresses that should be cc'd based on the changed
1019 files in this CL.
1020 """
1021 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001022
1023 def GetBranch(self):
1024 """Returns the short branch name, e.g. 'master'."""
1025 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001026 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001027 if not branchref:
1028 return None
1029 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001030 self.branch = ShortBranchName(self.branchref)
1031 return self.branch
1032
1033 def GetBranchRef(self):
1034 """Returns the full branch name, e.g. 'refs/heads/master'."""
1035 self.GetBranch() # Poke the lazy loader.
1036 return self.branchref
1037
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001038 def ClearBranch(self):
1039 """Clears cached branch data of this object."""
1040 self.branch = self.branchref = None
1041
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001042 @staticmethod
1043 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001044 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001045 e.g. 'origin', 'refs/heads/master'
1046 """
1047 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001048 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
1049 error_ok=True).strip()
1050 if upstream_branch:
1051 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
1052 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001053 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1054 error_ok=True).strip()
1055 if upstream_branch:
1056 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001057 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001058 # Fall back on trying a git-svn upstream branch.
1059 if settings.GetIsGitSvn():
1060 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001061 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001062 # Else, try to guess the origin remote.
1063 remote_branches = RunGit(['branch', '-r']).split()
1064 if 'origin/master' in remote_branches:
1065 # Fall back on origin/master if it exits.
1066 remote = 'origin'
1067 upstream_branch = 'refs/heads/master'
1068 elif 'origin/trunk' in remote_branches:
1069 # Fall back on origin/trunk if it exists. Generally a shared
1070 # git-svn clone
1071 remote = 'origin'
1072 upstream_branch = 'refs/heads/trunk'
1073 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001074 DieWithError(
1075 'Unable to determine default branch to diff against.\n'
1076 'Either pass complete "git diff"-style arguments, like\n'
1077 ' git cl upload origin/master\n'
1078 'or verify this branch is set up to track another \n'
1079 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001080
1081 return remote, upstream_branch
1082
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001083 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001084 upstream_branch = self.GetUpstreamBranch()
1085 if not BranchExists(upstream_branch):
1086 DieWithError('The upstream for the current branch (%s) does not exist '
1087 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001088 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001089 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001090
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001091 def GetUpstreamBranch(self):
1092 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001093 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001094 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001095 upstream_branch = upstream_branch.replace('refs/heads/',
1096 'refs/remotes/%s/' % remote)
1097 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1098 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001099 self.upstream_branch = upstream_branch
1100 return self.upstream_branch
1101
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001102 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001103 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001104 remote, branch = None, self.GetBranch()
1105 seen_branches = set()
1106 while branch not in seen_branches:
1107 seen_branches.add(branch)
1108 remote, branch = self.FetchUpstreamTuple(branch)
1109 branch = ShortBranchName(branch)
1110 if remote != '.' or branch.startswith('refs/remotes'):
1111 break
1112 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001113 remotes = RunGit(['remote'], error_ok=True).split()
1114 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001115 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001116 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001117 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001118 logging.warning('Could not determine which remote this change is '
1119 'associated with, so defaulting to "%s". This may '
1120 'not be what you want. You may prevent this message '
1121 'by running "git svn info" as documented here: %s',
1122 self._remote,
1123 GIT_INSTRUCTIONS_URL)
1124 else:
1125 logging.warn('Could not determine which remote this change is '
1126 'associated with. You may prevent this message by '
1127 'running "git svn info" as documented here: %s',
1128 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001129 branch = 'HEAD'
1130 if branch.startswith('refs/remotes'):
1131 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001132 elif branch.startswith('refs/branch-heads/'):
1133 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001134 else:
1135 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001136 return self._remote
1137
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001138 def GitSanityChecks(self, upstream_git_obj):
1139 """Checks git repo status and ensures diff is from local commits."""
1140
sbc@chromium.org79706062015-01-14 21:18:12 +00001141 if upstream_git_obj is None:
1142 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001143 print('ERROR: unable to determine current branch (detached HEAD?)',
1144 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001145 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001146 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001147 return False
1148
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001149 # Verify the commit we're diffing against is in our current branch.
1150 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1151 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1152 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001153 print('ERROR: %s is not in the current branch. You may need to rebase '
1154 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001155 return False
1156
1157 # List the commits inside the diff, and verify they are all local.
1158 commits_in_diff = RunGit(
1159 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1160 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1161 remote_branch = remote_branch.strip()
1162 if code != 0:
1163 _, remote_branch = self.GetRemoteBranch()
1164
1165 commits_in_remote = RunGit(
1166 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1167
1168 common_commits = set(commits_in_diff) & set(commits_in_remote)
1169 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001170 print('ERROR: Your diff contains %d commits already in %s.\n'
1171 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1172 'the diff. If you are using a custom git flow, you can override'
1173 ' the reference used for this check with "git config '
1174 'gitcl.remotebranch <git-ref>".' % (
1175 len(common_commits), remote_branch, upstream_git_obj),
1176 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001177 return False
1178 return True
1179
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001180 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001181 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001182
1183 Returns None if it is not set.
1184 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001185 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
1186 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001187
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001188 def GetGitSvnRemoteUrl(self):
1189 """Return the configured git-svn remote URL parsed from git svn info.
1190
1191 Returns None if it is not set.
1192 """
1193 # URL is dependent on the current directory.
1194 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1195 if data:
1196 keys = dict(line.split(': ', 1) for line in data.splitlines()
1197 if ': ' in line)
1198 return keys.get('URL', None)
1199 return None
1200
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001201 def GetRemoteUrl(self):
1202 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1203
1204 Returns None if there is no remote.
1205 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001206 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001207 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1208
1209 # If URL is pointing to a local directory, it is probably a git cache.
1210 if os.path.isdir(url):
1211 url = RunGit(['config', 'remote.%s.url' % remote],
1212 error_ok=True,
1213 cwd=url).strip()
1214 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001215
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001216 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001217 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001218 if self.issue is None and not self.lookedup_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001219 issue = RunGit(['config',
1220 self._codereview_impl.IssueSetting(self.GetBranch())],
1221 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001222 self.issue = int(issue) or None if issue else None
1223 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001224 return self.issue
1225
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001226 def GetIssueURL(self):
1227 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001228 issue = self.GetIssue()
1229 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001230 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001231 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001232
1233 def GetDescription(self, pretty=False):
1234 if not self.has_description:
1235 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001236 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001237 self.has_description = True
1238 if pretty:
1239 wrapper = textwrap.TextWrapper()
1240 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1241 return wrapper.fill(self.description)
1242 return self.description
1243
1244 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001245 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001246 if self.patchset is None and not self.lookedup_patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001247 patchset = RunGit(['config', self._codereview_impl.PatchsetSetting()],
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001248 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001249 self.patchset = int(patchset) or None if patchset else None
1250 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001251 return self.patchset
1252
1253 def SetPatchset(self, patchset):
1254 """Set this branch's patchset. If patchset=0, clears the patchset."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001255 patchset_setting = self._codereview_impl.PatchsetSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001256 if patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001257 RunGit(['config', patchset_setting, str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001258 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001259 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001260 RunGit(['config', '--unset', patchset_setting],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001261 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001262 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001263
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001264 def SetIssue(self, issue=None):
1265 """Set this branch's issue. If issue isn't given, clears the issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001266 issue_setting = self._codereview_impl.IssueSetting(self.GetBranch())
1267 codereview_setting = self._codereview_impl.GetCodereviewServerSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001268 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001269 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001270 RunGit(['config', issue_setting, str(issue)])
1271 codereview_server = self._codereview_impl.GetCodereviewServer()
1272 if codereview_server:
1273 RunGit(['config', codereview_setting, codereview_server])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001274 else:
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001275 # Reset it regardless. It doesn't hurt.
1276 config_settings = [issue_setting, self._codereview_impl.PatchsetSetting()]
1277 for prop in (['last-upload-hash'] +
1278 self._codereview_impl._PostUnsetIssueProperties()):
1279 config_settings.append('branch.%s.%s' % (self.GetBranch(), prop))
1280 for setting in config_settings:
1281 RunGit(['config', '--unset', setting], error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001282 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001283 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001284
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001285 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001286 if not self.GitSanityChecks(upstream_branch):
1287 DieWithError('\nGit sanity check failure')
1288
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001289 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001290 if not root:
1291 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001292 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001293
1294 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001295 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001296 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001297 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001298 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001299 except subprocess2.CalledProcessError:
1300 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001301 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001302 'This branch probably doesn\'t exist anymore. To reset the\n'
1303 'tracking branch, please run\n'
1304 ' git branch --set-upstream %s trunk\n'
1305 'replacing trunk with origin/master or the relevant branch') %
1306 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001307
maruel@chromium.org52424302012-08-29 15:14:30 +00001308 issue = self.GetIssue()
1309 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001310 if issue:
1311 description = self.GetDescription()
1312 else:
1313 # If the change was never uploaded, use the log messages of all commits
1314 # up to the branch point, as git cl upload will prefill the description
1315 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001316 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1317 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001318
1319 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001320 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001321 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001322 name,
1323 description,
1324 absroot,
1325 files,
1326 issue,
1327 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001328 author,
1329 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001330
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001331 def UpdateDescription(self, description):
1332 self.description = description
1333 return self._codereview_impl.UpdateDescriptionRemote(description)
1334
1335 def RunHook(self, committing, may_prompt, verbose, change):
1336 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1337 try:
1338 return presubmit_support.DoPresubmitChecks(change, committing,
1339 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1340 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001341 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1342 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001343 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001344 DieWithError(
1345 ('%s\nMaybe your depot_tools is out of date?\n'
1346 'If all fails, contact maruel@') % e)
1347
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001348 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1349 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001350 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1351 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001352 else:
1353 # Assume url.
1354 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1355 urlparse.urlparse(issue_arg))
1356 if not parsed_issue_arg or not parsed_issue_arg.valid:
1357 DieWithError('Failed to parse issue argument "%s". '
1358 'Must be an issue number or a valid URL.' % issue_arg)
1359 return self._codereview_impl.CMDPatchWithParsedIssue(
1360 parsed_issue_arg, reject, nocommit, directory)
1361
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001362 def CMDUpload(self, options, git_diff_args, orig_args):
1363 """Uploads a change to codereview."""
1364 if git_diff_args:
1365 # TODO(ukai): is it ok for gerrit case?
1366 base_branch = git_diff_args[0]
1367 else:
1368 if self.GetBranch() is None:
1369 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1370
1371 # Default to diffing against common ancestor of upstream branch
1372 base_branch = self.GetCommonAncestorWithUpstream()
1373 git_diff_args = [base_branch, 'HEAD']
1374
1375 # Make sure authenticated to codereview before running potentially expensive
1376 # hooks. It is a fast, best efforts check. Codereview still can reject the
1377 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001378 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001379
1380 # Apply watchlists on upload.
1381 change = self.GetChange(base_branch, None)
1382 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1383 files = [f.LocalPath() for f in change.AffectedFiles()]
1384 if not options.bypass_watchlists:
1385 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1386
1387 if not options.bypass_hooks:
1388 if options.reviewers or options.tbr_owners:
1389 # Set the reviewer list now so that presubmit checks can access it.
1390 change_description = ChangeDescription(change.FullDescriptionText())
1391 change_description.update_reviewers(options.reviewers,
1392 options.tbr_owners,
1393 change)
1394 change.SetDescriptionText(change_description.description)
1395 hook_results = self.RunHook(committing=False,
1396 may_prompt=not options.force,
1397 verbose=options.verbose,
1398 change=change)
1399 if not hook_results.should_continue():
1400 return 1
1401 if not options.reviewers and hook_results.reviewers:
1402 options.reviewers = hook_results.reviewers.split(',')
1403
1404 if self.GetIssue():
1405 latest_patchset = self.GetMostRecentPatchset()
1406 local_patchset = self.GetPatchset()
1407 if (latest_patchset and local_patchset and
1408 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001409 print('The last upload made from this repository was patchset #%d but '
1410 'the most recent patchset on the server is #%d.'
1411 % (local_patchset, latest_patchset))
1412 print('Uploading will still work, but if you\'ve uploaded to this '
1413 'issue from another machine or branch the patch you\'re '
1414 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001415 ask_for_data('About to upload; enter to confirm.')
1416
1417 print_stats(options.similarity, options.find_copies, git_diff_args)
1418 ret = self.CMDUploadChange(options, git_diff_args, change)
1419 if not ret:
1420 git_set_branch_value('last-upload-hash',
1421 RunGit(['rev-parse', 'HEAD']).strip())
1422 # Run post upload hooks, if specified.
1423 if settings.GetRunPostUploadHook():
1424 presubmit_support.DoPostUploadExecuter(
1425 change,
1426 self,
1427 settings.GetRoot(),
1428 options.verbose,
1429 sys.stdout)
1430
1431 # Upload all dependencies if specified.
1432 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001433 print()
1434 print('--dependencies has been specified.')
1435 print('All dependent local branches will be re-uploaded.')
1436 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001437 # Remove the dependencies flag from args so that we do not end up in a
1438 # loop.
1439 orig_args.remove('--dependencies')
1440 ret = upload_branch_deps(self, orig_args)
1441 return ret
1442
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001443 def SetCQState(self, new_state):
1444 """Update the CQ state for latest patchset.
1445
1446 Issue must have been already uploaded and known.
1447 """
1448 assert new_state in _CQState.ALL_STATES
1449 assert self.GetIssue()
1450 return self._codereview_impl.SetCQState(new_state)
1451
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001452 # Forward methods to codereview specific implementation.
1453
1454 def CloseIssue(self):
1455 return self._codereview_impl.CloseIssue()
1456
1457 def GetStatus(self):
1458 return self._codereview_impl.GetStatus()
1459
1460 def GetCodereviewServer(self):
1461 return self._codereview_impl.GetCodereviewServer()
1462
1463 def GetApprovingReviewers(self):
1464 return self._codereview_impl.GetApprovingReviewers()
1465
1466 def GetMostRecentPatchset(self):
1467 return self._codereview_impl.GetMostRecentPatchset()
1468
1469 def __getattr__(self, attr):
1470 # This is because lots of untested code accesses Rietveld-specific stuff
1471 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001472 # on a case by case basis.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001473 return getattr(self._codereview_impl, attr)
1474
1475
1476class _ChangelistCodereviewBase(object):
1477 """Abstract base class encapsulating codereview specifics of a changelist."""
1478 def __init__(self, changelist):
1479 self._changelist = changelist # instance of Changelist
1480
1481 def __getattr__(self, attr):
1482 # Forward methods to changelist.
1483 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1484 # _RietveldChangelistImpl to avoid this hack?
1485 return getattr(self._changelist, attr)
1486
1487 def GetStatus(self):
1488 """Apply a rough heuristic to give a simple summary of an issue's review
1489 or CQ status, assuming adherence to a common workflow.
1490
1491 Returns None if no issue for this branch, or specific string keywords.
1492 """
1493 raise NotImplementedError()
1494
1495 def GetCodereviewServer(self):
1496 """Returns server URL without end slash, like "https://codereview.com"."""
1497 raise NotImplementedError()
1498
1499 def FetchDescription(self):
1500 """Fetches and returns description from the codereview server."""
1501 raise NotImplementedError()
1502
1503 def GetCodereviewServerSetting(self):
1504 """Returns git config setting for the codereview server."""
1505 raise NotImplementedError()
1506
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001507 @classmethod
1508 def IssueSetting(cls, branch):
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001509 return 'branch.%s.%s' % (branch, cls.IssueSettingSuffix())
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001510
1511 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001512 def IssueSettingSuffix(cls):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001513 """Returns name of git config setting which stores issue number for a given
1514 branch."""
1515 raise NotImplementedError()
1516
1517 def PatchsetSetting(self):
1518 """Returns name of git config setting which stores issue number."""
1519 raise NotImplementedError()
1520
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001521 def _PostUnsetIssueProperties(self):
1522 """Which branch-specific properties to erase when unsettin issue."""
1523 raise NotImplementedError()
1524
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001525 def GetRieveldObjForPresubmit(self):
1526 # This is an unfortunate Rietveld-embeddedness in presubmit.
1527 # For non-Rietveld codereviews, this probably should return a dummy object.
1528 raise NotImplementedError()
1529
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001530 def GetGerritObjForPresubmit(self):
1531 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1532 return None
1533
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001534 def UpdateDescriptionRemote(self, description):
1535 """Update the description on codereview site."""
1536 raise NotImplementedError()
1537
1538 def CloseIssue(self):
1539 """Closes the issue."""
1540 raise NotImplementedError()
1541
1542 def GetApprovingReviewers(self):
1543 """Returns a list of reviewers approving the change.
1544
1545 Note: not necessarily committers.
1546 """
1547 raise NotImplementedError()
1548
1549 def GetMostRecentPatchset(self):
1550 """Returns the most recent patchset number from the codereview site."""
1551 raise NotImplementedError()
1552
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001553 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1554 directory):
1555 """Fetches and applies the issue.
1556
1557 Arguments:
1558 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1559 reject: if True, reject the failed patch instead of switching to 3-way
1560 merge. Rietveld only.
1561 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1562 only.
1563 directory: switch to directory before applying the patch. Rietveld only.
1564 """
1565 raise NotImplementedError()
1566
1567 @staticmethod
1568 def ParseIssueURL(parsed_url):
1569 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1570 failed."""
1571 raise NotImplementedError()
1572
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001573 def EnsureAuthenticated(self, force):
1574 """Best effort check that user is authenticated with codereview server.
1575
1576 Arguments:
1577 force: whether to skip confirmation questions.
1578 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001579 raise NotImplementedError()
1580
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001581 def CMDUploadChange(self, options, args, change):
1582 """Uploads a change to codereview."""
1583 raise NotImplementedError()
1584
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001585 def SetCQState(self, new_state):
1586 """Update the CQ state for latest patchset.
1587
1588 Issue must have been already uploaded and known.
1589 """
1590 raise NotImplementedError()
1591
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001592
1593class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1594 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1595 super(_RietveldChangelistImpl, self).__init__(changelist)
1596 assert settings, 'must be initialized in _ChangelistCodereviewBase'
1597 settings.GetDefaultServerUrl()
1598
1599 self._rietveld_server = rietveld_server
1600 self._auth_config = auth_config
1601 self._props = None
1602 self._rpc_server = None
1603
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001604 def GetCodereviewServer(self):
1605 if not self._rietveld_server:
1606 # If we're on a branch then get the server potentially associated
1607 # with that branch.
1608 if self.GetIssue():
1609 rietveld_server_setting = self.GetCodereviewServerSetting()
1610 if rietveld_server_setting:
1611 self._rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1612 ['config', rietveld_server_setting], error_ok=True).strip())
1613 if not self._rietveld_server:
1614 self._rietveld_server = settings.GetDefaultServerUrl()
1615 return self._rietveld_server
1616
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001617 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001618 """Best effort check that user is authenticated with Rietveld server."""
1619 if self._auth_config.use_oauth2:
1620 authenticator = auth.get_authenticator_for_host(
1621 self.GetCodereviewServer(), self._auth_config)
1622 if not authenticator.has_cached_credentials():
1623 raise auth.LoginRequiredError(self.GetCodereviewServer())
1624
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001625 def FetchDescription(self):
1626 issue = self.GetIssue()
1627 assert issue
1628 try:
1629 return self.RpcServer().get_description(issue).strip()
1630 except urllib2.HTTPError as e:
1631 if e.code == 404:
1632 DieWithError(
1633 ('\nWhile fetching the description for issue %d, received a '
1634 '404 (not found)\n'
1635 'error. It is likely that you deleted this '
1636 'issue on the server. If this is the\n'
1637 'case, please run\n\n'
1638 ' git cl issue 0\n\n'
1639 'to clear the association with the deleted issue. Then run '
1640 'this command again.') % issue)
1641 else:
1642 DieWithError(
1643 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1644 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001645 print('Warning: Failed to retrieve CL description due to network '
1646 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001647 return ''
1648
1649 def GetMostRecentPatchset(self):
1650 return self.GetIssueProperties()['patchsets'][-1]
1651
1652 def GetPatchSetDiff(self, issue, patchset):
1653 return self.RpcServer().get(
1654 '/download/issue%s_%s.diff' % (issue, patchset))
1655
1656 def GetIssueProperties(self):
1657 if self._props is None:
1658 issue = self.GetIssue()
1659 if not issue:
1660 self._props = {}
1661 else:
1662 self._props = self.RpcServer().get_issue_properties(issue, True)
1663 return self._props
1664
1665 def GetApprovingReviewers(self):
1666 return get_approving_reviewers(self.GetIssueProperties())
1667
1668 def AddComment(self, message):
1669 return self.RpcServer().add_comment(self.GetIssue(), message)
1670
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001671 def GetStatus(self):
1672 """Apply a rough heuristic to give a simple summary of an issue's review
1673 or CQ status, assuming adherence to a common workflow.
1674
1675 Returns None if no issue for this branch, or one of the following keywords:
1676 * 'error' - error from review tool (including deleted issues)
1677 * 'unsent' - not sent for review
1678 * 'waiting' - waiting for review
1679 * 'reply' - waiting for owner to reply to review
1680 * 'lgtm' - LGTM from at least one approved reviewer
1681 * 'commit' - in the commit queue
1682 * 'closed' - closed
1683 """
1684 if not self.GetIssue():
1685 return None
1686
1687 try:
1688 props = self.GetIssueProperties()
1689 except urllib2.HTTPError:
1690 return 'error'
1691
1692 if props.get('closed'):
1693 # Issue is closed.
1694 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001695 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001696 # Issue is in the commit queue.
1697 return 'commit'
1698
1699 try:
1700 reviewers = self.GetApprovingReviewers()
1701 except urllib2.HTTPError:
1702 return 'error'
1703
1704 if reviewers:
1705 # Was LGTM'ed.
1706 return 'lgtm'
1707
1708 messages = props.get('messages') or []
1709
1710 if not messages:
1711 # No message was sent.
1712 return 'unsent'
1713 if messages[-1]['sender'] != props.get('owner_email'):
1714 # Non-LGTM reply from non-owner
1715 return 'reply'
1716 return 'waiting'
1717
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001718 def UpdateDescriptionRemote(self, description):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001719 return self.RpcServer().update_description(
1720 self.GetIssue(), self.description)
1721
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001722 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001723 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001724
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001725 def SetFlag(self, flag, value):
1726 """Patchset must match."""
1727 if not self.GetPatchset():
1728 DieWithError('The patchset needs to match. Send another patchset.')
1729 try:
1730 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001731 self.GetIssue(), self.GetPatchset(), flag, value)
vapierfd77ac72016-06-16 08:33:57 -07001732 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001733 if e.code == 404:
1734 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1735 if e.code == 403:
1736 DieWithError(
1737 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1738 'match?') % (self.GetIssue(), self.GetPatchset()))
1739 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001740
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001741 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001742 """Returns an upload.RpcServer() to access this review's rietveld instance.
1743 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001744 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001745 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001746 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001747 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001748 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001749
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001750 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001751 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001752 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001753
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001754 def PatchsetSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001755 """Return the git setting that stores this change's most recent patchset."""
1756 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1757
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001758 def GetCodereviewServerSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001759 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001760 branch = self.GetBranch()
1761 if branch:
1762 return 'branch.%s.rietveldserver' % branch
1763 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001764
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001765 def _PostUnsetIssueProperties(self):
1766 """Which branch-specific properties to erase when unsetting issue."""
1767 return ['rietveldserver']
1768
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001769 def GetRieveldObjForPresubmit(self):
1770 return self.RpcServer()
1771
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001772 def SetCQState(self, new_state):
1773 props = self.GetIssueProperties()
1774 if props.get('private'):
1775 DieWithError('Cannot set-commit on private issue')
1776
1777 if new_state == _CQState.COMMIT:
1778 self.SetFlag('commit', '1')
1779 elif new_state == _CQState.NONE:
1780 self.SetFlag('commit', '0')
1781 else:
1782 raise NotImplementedError()
1783
1784
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001785 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1786 directory):
1787 # TODO(maruel): Use apply_issue.py
1788
1789 # PatchIssue should never be called with a dirty tree. It is up to the
1790 # caller to check this, but just in case we assert here since the
1791 # consequences of the caller not checking this could be dire.
1792 assert(not git_common.is_dirty_git_tree('apply'))
1793 assert(parsed_issue_arg.valid)
1794 self._changelist.issue = parsed_issue_arg.issue
1795 if parsed_issue_arg.hostname:
1796 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1797
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001798 if (isinstance(parsed_issue_arg, _RietveldParsedIssueNumberArgument) and
1799 parsed_issue_arg.patch_url):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001800 assert parsed_issue_arg.patchset
1801 patchset = parsed_issue_arg.patchset
1802 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1803 else:
1804 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1805 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1806
1807 # Switch up to the top-level directory, if necessary, in preparation for
1808 # applying the patch.
1809 top = settings.GetRelativeRoot()
1810 if top:
1811 os.chdir(top)
1812
1813 # Git patches have a/ at the beginning of source paths. We strip that out
1814 # with a sed script rather than the -p flag to patch so we can feed either
1815 # Git or svn-style patches into the same apply command.
1816 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1817 try:
1818 patch_data = subprocess2.check_output(
1819 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1820 except subprocess2.CalledProcessError:
1821 DieWithError('Git patch mungling failed.')
1822 logging.info(patch_data)
1823
1824 # We use "git apply" to apply the patch instead of "patch" so that we can
1825 # pick up file adds.
1826 # The --index flag means: also insert into the index (so we catch adds).
1827 cmd = ['git', 'apply', '--index', '-p0']
1828 if directory:
1829 cmd.extend(('--directory', directory))
1830 if reject:
1831 cmd.append('--reject')
1832 elif IsGitVersionAtLeast('1.7.12'):
1833 cmd.append('--3way')
1834 try:
1835 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
1836 stdin=patch_data, stdout=subprocess2.VOID)
1837 except subprocess2.CalledProcessError:
vapiera7fbd5a2016-06-16 09:17:49 -07001838 print('Failed to apply the patch')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001839 return 1
1840
1841 # If we had an issue, commit the current state and register the issue.
1842 if not nocommit:
1843 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
1844 'patch from issue %(i)s at patchset '
1845 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
1846 % {'i': self.GetIssue(), 'p': patchset})])
1847 self.SetIssue(self.GetIssue())
1848 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07001849 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001850 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001851 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001852 return 0
1853
1854 @staticmethod
1855 def ParseIssueURL(parsed_url):
1856 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1857 return None
1858 # Typical url: https://domain/<issue_number>[/[other]]
1859 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
1860 if match:
1861 return _RietveldParsedIssueNumberArgument(
1862 issue=int(match.group(1)),
1863 hostname=parsed_url.netloc)
1864 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
1865 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
1866 if match:
1867 return _RietveldParsedIssueNumberArgument(
1868 issue=int(match.group(1)),
1869 patchset=int(match.group(2)),
1870 hostname=parsed_url.netloc,
1871 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
1872 return None
1873
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001874 def CMDUploadChange(self, options, args, change):
1875 """Upload the patch to Rietveld."""
1876 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1877 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001878 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
1879 if options.emulate_svn_auto_props:
1880 upload_args.append('--emulate_svn_auto_props')
1881
1882 change_desc = None
1883
1884 if options.email is not None:
1885 upload_args.extend(['--email', options.email])
1886
1887 if self.GetIssue():
1888 if options.title:
1889 upload_args.extend(['--title', options.title])
1890 if options.message:
1891 upload_args.extend(['--message', options.message])
1892 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07001893 print('This branch is associated with issue %s. '
1894 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001895 else:
1896 if options.title:
1897 upload_args.extend(['--title', options.title])
1898 message = (options.title or options.message or
1899 CreateDescriptionFromLog(args))
1900 change_desc = ChangeDescription(message)
1901 if options.reviewers or options.tbr_owners:
1902 change_desc.update_reviewers(options.reviewers,
1903 options.tbr_owners,
1904 change)
1905 if not options.force:
1906 change_desc.prompt()
1907
1908 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07001909 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001910 return 1
1911
1912 upload_args.extend(['--message', change_desc.description])
1913 if change_desc.get_reviewers():
1914 upload_args.append('--reviewers=%s' % ','.join(
1915 change_desc.get_reviewers()))
1916 if options.send_mail:
1917 if not change_desc.get_reviewers():
1918 DieWithError("Must specify reviewers to send email.")
1919 upload_args.append('--send_mail')
1920
1921 # We check this before applying rietveld.private assuming that in
1922 # rietveld.cc only addresses which we can send private CLs to are listed
1923 # if rietveld.private is set, and so we should ignore rietveld.cc only
1924 # when --private is specified explicitly on the command line.
1925 if options.private:
1926 logging.warn('rietveld.cc is ignored since private flag is specified. '
1927 'You need to review and add them manually if necessary.')
1928 cc = self.GetCCListWithoutDefault()
1929 else:
1930 cc = self.GetCCList()
1931 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
1932 if cc:
1933 upload_args.extend(['--cc', cc])
1934
1935 if options.private or settings.GetDefaultPrivateFlag() == "True":
1936 upload_args.append('--private')
1937
1938 upload_args.extend(['--git_similarity', str(options.similarity)])
1939 if not options.find_copies:
1940 upload_args.extend(['--git_no_find_copies'])
1941
1942 # Include the upstream repo's URL in the change -- this is useful for
1943 # projects that have their source spread across multiple repos.
1944 remote_url = self.GetGitBaseUrlFromConfig()
1945 if not remote_url:
1946 if settings.GetIsGitSvn():
1947 remote_url = self.GetGitSvnRemoteUrl()
1948 else:
1949 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
1950 remote_url = '%s@%s' % (self.GetRemoteUrl(),
1951 self.GetUpstreamBranch().split('/')[-1])
1952 if remote_url:
1953 upload_args.extend(['--base_url', remote_url])
1954 remote, remote_branch = self.GetRemoteBranch()
1955 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
1956 settings.GetPendingRefPrefix())
1957 if target_ref:
1958 upload_args.extend(['--target_ref', target_ref])
1959
1960 # Look for dependent patchsets. See crbug.com/480453 for more details.
1961 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
1962 upstream_branch = ShortBranchName(upstream_branch)
1963 if remote is '.':
1964 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001965 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001966 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07001967 print()
1968 print('Skipping dependency patchset upload because git config '
1969 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
1970 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001971 else:
1972 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001973 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001974 auth_config=auth_config)
1975 branch_cl_issue_url = branch_cl.GetIssueURL()
1976 branch_cl_issue = branch_cl.GetIssue()
1977 branch_cl_patchset = branch_cl.GetPatchset()
1978 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
1979 upload_args.extend(
1980 ['--depends_on_patchset', '%s:%s' % (
1981 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001982 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001983 '\n'
1984 'The current branch (%s) is tracking a local branch (%s) with '
1985 'an associated CL.\n'
1986 'Adding %s/#ps%s as a dependency patchset.\n'
1987 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
1988 branch_cl_patchset))
1989
1990 project = settings.GetProject()
1991 if project:
1992 upload_args.extend(['--project', project])
1993
1994 if options.cq_dry_run:
1995 upload_args.extend(['--cq_dry_run'])
1996
1997 try:
1998 upload_args = ['upload'] + upload_args + args
1999 logging.info('upload.RealMain(%s)', upload_args)
2000 issue, patchset = upload.RealMain(upload_args)
2001 issue = int(issue)
2002 patchset = int(patchset)
2003 except KeyboardInterrupt:
2004 sys.exit(1)
2005 except:
2006 # If we got an exception after the user typed a description for their
2007 # change, back up the description before re-raising.
2008 if change_desc:
2009 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2010 print('\nGot exception while uploading -- saving description to %s\n' %
2011 backup_path)
2012 backup_file = open(backup_path, 'w')
2013 backup_file.write(change_desc.description)
2014 backup_file.close()
2015 raise
2016
2017 if not self.GetIssue():
2018 self.SetIssue(issue)
2019 self.SetPatchset(patchset)
2020
2021 if options.use_commit_queue:
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002022 self.SetCQState(_CQState.COMMIT)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002023 return 0
2024
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002025
2026class _GerritChangelistImpl(_ChangelistCodereviewBase):
2027 def __init__(self, changelist, auth_config=None):
2028 # auth_config is Rietveld thing, kept here to preserve interface only.
2029 super(_GerritChangelistImpl, self).__init__(changelist)
2030 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002031 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002032 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002033 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002034
2035 def _GetGerritHost(self):
2036 # Lazy load of configs.
2037 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002038 if self._gerrit_host and '.' not in self._gerrit_host:
2039 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2040 # This happens for internal stuff http://crbug.com/614312.
2041 parsed = urlparse.urlparse(self.GetRemoteUrl())
2042 if parsed.scheme == 'sso':
2043 print('WARNING: using non https URLs for remote is likely broken\n'
2044 ' Your current remote is: %s' % self.GetRemoteUrl())
2045 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2046 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002047 return self._gerrit_host
2048
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002049 def _GetGitHost(self):
2050 """Returns git host to be used when uploading change to Gerrit."""
2051 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2052
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002053 def GetCodereviewServer(self):
2054 if not self._gerrit_server:
2055 # If we're on a branch then get the server potentially associated
2056 # with that branch.
2057 if self.GetIssue():
2058 gerrit_server_setting = self.GetCodereviewServerSetting()
2059 if gerrit_server_setting:
2060 self._gerrit_server = RunGit(['config', gerrit_server_setting],
2061 error_ok=True).strip()
2062 if self._gerrit_server:
2063 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
2064 if not self._gerrit_server:
2065 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2066 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002067 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002068 parts[0] = parts[0] + '-review'
2069 self._gerrit_host = '.'.join(parts)
2070 self._gerrit_server = 'https://%s' % self._gerrit_host
2071 return self._gerrit_server
2072
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002073 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00002074 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002075 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002076
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002077 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002078 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002079 if settings.GetGerritSkipEnsureAuthenticated():
2080 # For projects with unusual authentication schemes.
2081 # See http://crbug.com/603378.
2082 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002083 # Lazy-loader to identify Gerrit and Git hosts.
2084 if gerrit_util.GceAuthenticator.is_gce():
2085 return
2086 self.GetCodereviewServer()
2087 git_host = self._GetGitHost()
2088 assert self._gerrit_server and self._gerrit_host
2089 cookie_auth = gerrit_util.CookiesAuthenticator()
2090
2091 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2092 git_auth = cookie_auth.get_auth_header(git_host)
2093 if gerrit_auth and git_auth:
2094 if gerrit_auth == git_auth:
2095 return
2096 print((
2097 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2098 ' Check your %s or %s file for credentials of hosts:\n'
2099 ' %s\n'
2100 ' %s\n'
2101 ' %s') %
2102 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2103 git_host, self._gerrit_host,
2104 cookie_auth.get_new_password_message(git_host)))
2105 if not force:
2106 ask_for_data('If you know what you are doing, press Enter to continue, '
2107 'Ctrl+C to abort.')
2108 return
2109 else:
2110 missing = (
2111 [] if gerrit_auth else [self._gerrit_host] +
2112 [] if git_auth else [git_host])
2113 DieWithError('Credentials for the following hosts are required:\n'
2114 ' %s\n'
2115 'These are read from %s (or legacy %s)\n'
2116 '%s' % (
2117 '\n '.join(missing),
2118 cookie_auth.get_gitcookies_path(),
2119 cookie_auth.get_netrc_path(),
2120 cookie_auth.get_new_password_message(git_host)))
2121
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002122
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002123 def PatchsetSetting(self):
2124 """Return the git setting that stores this change's most recent patchset."""
2125 return 'branch.%s.gerritpatchset' % self.GetBranch()
2126
2127 def GetCodereviewServerSetting(self):
2128 """Returns the git setting that stores this change's Gerrit server."""
2129 branch = self.GetBranch()
2130 if branch:
2131 return 'branch.%s.gerritserver' % branch
2132 return None
2133
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002134 def _PostUnsetIssueProperties(self):
2135 """Which branch-specific properties to erase when unsetting issue."""
2136 return [
2137 'gerritserver',
2138 'gerritsquashhash',
2139 ]
2140
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002141 def GetRieveldObjForPresubmit(self):
2142 class ThisIsNotRietveldIssue(object):
2143 def __nonzero__(self):
2144 # This is a hack to make presubmit_support think that rietveld is not
2145 # defined, yet still ensure that calls directly result in a decent
2146 # exception message below.
2147 return False
2148
2149 def __getattr__(self, attr):
2150 print(
2151 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2152 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2153 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2154 'or use Rietveld for codereview.\n'
2155 'See also http://crbug.com/579160.' % attr)
2156 raise NotImplementedError()
2157 return ThisIsNotRietveldIssue()
2158
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002159 def GetGerritObjForPresubmit(self):
2160 return presubmit_support.GerritAccessor(self._GetGerritHost())
2161
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002162 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002163 """Apply a rough heuristic to give a simple summary of an issue's review
2164 or CQ status, assuming adherence to a common workflow.
2165
2166 Returns None if no issue for this branch, or one of the following keywords:
2167 * 'error' - error from review tool (including deleted issues)
2168 * 'unsent' - no reviewers added
2169 * 'waiting' - waiting for review
2170 * 'reply' - waiting for owner to reply to review
2171 * 'not lgtm' - Code-Review -2 from at least one approved reviewer
2172 * 'lgtm' - Code-Review +2 from at least one approved reviewer
2173 * 'commit' - in the commit queue
2174 * 'closed' - abandoned
2175 """
2176 if not self.GetIssue():
2177 return None
2178
2179 try:
2180 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
2181 except httplib.HTTPException:
2182 return 'error'
2183
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002184 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002185 return 'closed'
2186
2187 cq_label = data['labels'].get('Commit-Queue', {})
2188 if cq_label:
2189 # Vote value is a stringified integer, which we expect from 0 to 2.
2190 vote_value = cq_label.get('value', '0')
2191 vote_text = cq_label.get('values', {}).get(vote_value, '')
2192 if vote_text.lower() == 'commit':
2193 return 'commit'
2194
2195 lgtm_label = data['labels'].get('Code-Review', {})
2196 if lgtm_label:
2197 if 'rejected' in lgtm_label:
2198 return 'not lgtm'
2199 if 'approved' in lgtm_label:
2200 return 'lgtm'
2201
2202 if not data.get('reviewers', {}).get('REVIEWER', []):
2203 return 'unsent'
2204
2205 messages = data.get('messages', [])
2206 if messages:
2207 owner = data['owner'].get('_account_id')
2208 last_message_author = messages[-1].get('author', {}).get('_account_id')
2209 if owner != last_message_author:
2210 # Some reply from non-owner.
2211 return 'reply'
2212
2213 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002214
2215 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002216 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002217 return data['revisions'][data['current_revision']]['_number']
2218
2219 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002220 data = self._GetChangeDetail(['CURRENT_REVISION'])
2221 current_rev = data['current_revision']
2222 url = data['revisions'][current_rev]['fetch']['http']['url']
2223 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002224
2225 def UpdateDescriptionRemote(self, description):
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002226 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
2227 description)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002228
2229 def CloseIssue(self):
2230 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2231
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002232 def GetApprovingReviewers(self):
2233 """Returns a list of reviewers approving the change.
2234
2235 Note: not necessarily committers.
2236 """
2237 raise NotImplementedError()
2238
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002239 def SubmitIssue(self, wait_for_merge=True):
2240 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2241 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002242
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002243 def _GetChangeDetail(self, options=None, issue=None):
2244 options = options or []
2245 issue = issue or self.GetIssue()
2246 assert issue, 'issue required to query Gerrit'
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002247 return gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2248 options)
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002249
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002250 def CMDLand(self, force, bypass_hooks, verbose):
2251 if git_common.is_dirty_git_tree('land'):
2252 return 1
2253 differs = True
2254 last_upload = RunGit(['config',
2255 'branch.%s.gerritsquashhash' % self.GetBranch()],
2256 error_ok=True).strip()
2257 # Note: git diff outputs nothing if there is no diff.
2258 if not last_upload or RunGit(['diff', last_upload]).strip():
2259 print('WARNING: some changes from local branch haven\'t been uploaded')
2260 else:
2261 detail = self._GetChangeDetail(['CURRENT_REVISION'])
2262 if detail['current_revision'] == last_upload:
2263 differs = False
2264 else:
2265 print('WARNING: local branch contents differ from latest uploaded '
2266 'patchset')
2267 if differs:
2268 if not force:
2269 ask_for_data(
2270 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2271 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2272 elif not bypass_hooks:
2273 hook_results = self.RunHook(
2274 committing=True,
2275 may_prompt=not force,
2276 verbose=verbose,
2277 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2278 if not hook_results.should_continue():
2279 return 1
2280
2281 self.SubmitIssue(wait_for_merge=True)
2282 print('Issue %s has been submitted.' % self.GetIssueURL())
2283 return 0
2284
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002285 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2286 directory):
2287 assert not reject
2288 assert not nocommit
2289 assert not directory
2290 assert parsed_issue_arg.valid
2291
2292 self._changelist.issue = parsed_issue_arg.issue
2293
2294 if parsed_issue_arg.hostname:
2295 self._gerrit_host = parsed_issue_arg.hostname
2296 self._gerrit_server = 'https://%s' % self._gerrit_host
2297
2298 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2299
2300 if not parsed_issue_arg.patchset:
2301 # Use current revision by default.
2302 revision_info = detail['revisions'][detail['current_revision']]
2303 patchset = int(revision_info['_number'])
2304 else:
2305 patchset = parsed_issue_arg.patchset
2306 for revision_info in detail['revisions'].itervalues():
2307 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2308 break
2309 else:
2310 DieWithError('Couldn\'t find patchset %i in issue %i' %
2311 (parsed_issue_arg.patchset, self.GetIssue()))
2312
2313 fetch_info = revision_info['fetch']['http']
2314 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2315 RunGit(['cherry-pick', 'FETCH_HEAD'])
2316 self.SetIssue(self.GetIssue())
2317 self.SetPatchset(patchset)
2318 print('Committed patch for issue %i pathset %i locally' %
2319 (self.GetIssue(), self.GetPatchset()))
2320 return 0
2321
2322 @staticmethod
2323 def ParseIssueURL(parsed_url):
2324 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2325 return None
2326 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2327 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2328 # Short urls like https://domain/<issue_number> can be used, but don't allow
2329 # specifying the patchset (you'd 404), but we allow that here.
2330 if parsed_url.path == '/':
2331 part = parsed_url.fragment
2332 else:
2333 part = parsed_url.path
2334 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2335 if match:
2336 return _ParsedIssueNumberArgument(
2337 issue=int(match.group(2)),
2338 patchset=int(match.group(4)) if match.group(4) else None,
2339 hostname=parsed_url.netloc)
2340 return None
2341
tandrii16e0b4e2016-06-07 10:34:28 -07002342 def _GerritCommitMsgHookCheck(self, offer_removal):
2343 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2344 if not os.path.exists(hook):
2345 return
2346 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2347 # custom developer made one.
2348 data = gclient_utils.FileRead(hook)
2349 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2350 return
2351 print('Warning: you have Gerrit commit-msg hook installed.\n'
2352 'It is not neccessary for uploading with git cl in squash mode, '
2353 'and may interfere with it in subtle ways.\n'
2354 'We recommend you remove the commit-msg hook.')
2355 if offer_removal:
2356 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2357 if reply.lower().startswith('y'):
2358 gclient_utils.rm_file_or_tree(hook)
2359 print('Gerrit commit-msg hook removed.')
2360 else:
2361 print('OK, will keep Gerrit commit-msg hook in place.')
2362
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002363 def CMDUploadChange(self, options, args, change):
2364 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002365 if options.squash and options.no_squash:
2366 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002367
2368 if not options.squash and not options.no_squash:
2369 # Load default for user, repo, squash=true, in this order.
2370 options.squash = settings.GetSquashGerritUploads()
2371 elif options.no_squash:
2372 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002373
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002374 # We assume the remote called "origin" is the one we want.
2375 # It is probably not worthwhile to support different workflows.
2376 gerrit_remote = 'origin'
2377
2378 remote, remote_branch = self.GetRemoteBranch()
2379 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2380 pending_prefix='')
2381
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002382 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002383 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002384 if not self.GetIssue():
2385 # TODO(tandrii): deperecate this after 2016Q2. Backwards compatibility
2386 # with shadow branch, which used to contain change-id for a given
2387 # branch, using which we can fetch actual issue number and set it as the
2388 # property of the branch, which is the new way.
2389 message = RunGitSilent([
2390 'show', '--format=%B', '-s',
2391 'refs/heads/git_cl_uploads/%s' % self.GetBranch()])
2392 if message:
2393 change_ids = git_footers.get_footer_change_id(message.strip())
2394 if change_ids and len(change_ids) == 1:
2395 details = self._GetChangeDetail(issue=change_ids[0])
2396 if details:
2397 print('WARNING: found old upload in branch git_cl_uploads/%s '
2398 'corresponding to issue %s' %
2399 (self.GetBranch(), details['_number']))
2400 self.SetIssue(details['_number'])
2401 if not self.GetIssue():
2402 DieWithError(
2403 '\n' # For readability of the blob below.
2404 'Found old upload in branch git_cl_uploads/%s, '
2405 'but failed to find corresponding Gerrit issue.\n'
2406 'If you know the issue number, set it manually first:\n'
2407 ' git cl issue 123456\n'
2408 'If you intended to upload this CL as new issue, '
2409 'just delete or rename the old upload branch:\n'
2410 ' git rename-branch git_cl_uploads/%s old_upload-%s\n'
2411 'After that, please run git cl upload again.' %
2412 tuple([self.GetBranch()] * 3))
2413 # End of backwards compatability.
2414
2415 if self.GetIssue():
2416 # Try to get the message from a previous upload.
2417 message = self.GetDescription()
2418 if not message:
2419 DieWithError(
2420 'failed to fetch description from current Gerrit issue %d\n'
2421 '%s' % (self.GetIssue(), self.GetIssueURL()))
2422 change_id = self._GetChangeDetail()['change_id']
2423 while True:
2424 footer_change_ids = git_footers.get_footer_change_id(message)
2425 if footer_change_ids == [change_id]:
2426 break
2427 if not footer_change_ids:
2428 message = git_footers.add_footer_change_id(message, change_id)
2429 print('WARNING: appended missing Change-Id to issue description')
2430 continue
2431 # There is already a valid footer but with different or several ids.
2432 # Doing this automatically is non-trivial as we don't want to lose
2433 # existing other footers, yet we want to append just 1 desired
2434 # Change-Id. Thus, just create a new footer, but let user verify the
2435 # new description.
2436 message = '%s\n\nChange-Id: %s' % (message, change_id)
2437 print(
2438 'WARNING: issue %s has Change-Id footer(s):\n'
2439 ' %s\n'
2440 'but issue has Change-Id %s, according to Gerrit.\n'
2441 'Please, check the proposed correction to the description, '
2442 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2443 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2444 change_id))
2445 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2446 if not options.force:
2447 change_desc = ChangeDescription(message)
2448 change_desc.prompt()
2449 message = change_desc.description
2450 if not message:
2451 DieWithError("Description is empty. Aborting...")
2452 # Continue the while loop.
2453 # Sanity check of this code - we should end up with proper message
2454 # footer.
2455 assert [change_id] == git_footers.get_footer_change_id(message)
2456 change_desc = ChangeDescription(message)
2457 else:
2458 change_desc = ChangeDescription(
2459 options.message or CreateDescriptionFromLog(args))
2460 if not options.force:
2461 change_desc.prompt()
2462 if not change_desc.description:
2463 DieWithError("Description is empty. Aborting...")
2464 message = change_desc.description
2465 change_ids = git_footers.get_footer_change_id(message)
2466 if len(change_ids) > 1:
2467 DieWithError('too many Change-Id footers, at most 1 allowed.')
2468 if not change_ids:
2469 # Generate the Change-Id automatically.
2470 message = git_footers.add_footer_change_id(
2471 message, GenerateGerritChangeId(message))
2472 change_desc.set_description(message)
2473 change_ids = git_footers.get_footer_change_id(message)
2474 assert len(change_ids) == 1
2475 change_id = change_ids[0]
2476
2477 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2478 if remote is '.':
2479 # If our upstream branch is local, we base our squashed commit on its
2480 # squashed version.
2481 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2482 # Check the squashed hash of the parent.
2483 parent = RunGit(['config',
2484 'branch.%s.gerritsquashhash' % upstream_branch_name],
2485 error_ok=True).strip()
2486 # Verify that the upstream branch has been uploaded too, otherwise
2487 # Gerrit will create additional CLs when uploading.
2488 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2489 RunGitSilent(['rev-parse', parent + ':'])):
2490 # TODO(tandrii): remove "old depot_tools" part on April 12, 2016.
2491 DieWithError(
2492 'Upload upstream branch %s first.\n'
2493 'Note: maybe you\'ve uploaded it with --no-squash or with an old '
2494 'version of depot_tools. If so, then re-upload it with:\n'
2495 ' git cl upload --squash\n' % upstream_branch_name)
2496 else:
2497 parent = self.GetCommonAncestorWithUpstream()
2498
2499 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2500 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2501 '-m', message]).strip()
2502 else:
2503 change_desc = ChangeDescription(
2504 options.message or CreateDescriptionFromLog(args))
2505 if not change_desc.description:
2506 DieWithError("Description is empty. Aborting...")
2507
2508 if not git_footers.get_footer_change_id(change_desc.description):
2509 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002510 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2511 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002512 ref_to_push = 'HEAD'
2513 parent = '%s/%s' % (gerrit_remote, branch)
2514 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2515
2516 assert change_desc
2517 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2518 ref_to_push)]).splitlines()
2519 if len(commits) > 1:
2520 print('WARNING: This will upload %d commits. Run the following command '
2521 'to see which commits will be uploaded: ' % len(commits))
2522 print('git log %s..%s' % (parent, ref_to_push))
2523 print('You can also use `git squash-branch` to squash these into a '
2524 'single commit.')
2525 ask_for_data('About to upload; enter to confirm.')
2526
2527 if options.reviewers or options.tbr_owners:
2528 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2529 change)
2530
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002531 # Extra options that can be specified at push time. Doc:
2532 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2533 refspec_opts = []
2534 if options.title:
2535 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2536 # reverse on its side.
2537 if '_' in options.title:
2538 print('WARNING: underscores in title will be converted to spaces.')
2539 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2540
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002541 if options.send_mail:
2542 if not change_desc.get_reviewers():
2543 DieWithError('Must specify reviewers to send email.')
2544 refspec_opts.append('notify=ALL')
2545 else:
2546 refspec_opts.append('notify=NONE')
2547
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002548 cc = self.GetCCList().split(',')
2549 if options.cc:
2550 cc.extend(options.cc)
2551 cc = filter(None, cc)
2552 if cc:
tandrii@chromium.org074c2af2016-06-03 23:18:40 +00002553 refspec_opts.extend('cc=' + email.strip() for email in cc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002554
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002555 if change_desc.get_reviewers():
2556 refspec_opts.extend('r=' + email.strip()
2557 for email in change_desc.get_reviewers())
2558
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002559 refspec_suffix = ''
2560 if refspec_opts:
2561 refspec_suffix = '%' + ','.join(refspec_opts)
2562 assert ' ' not in refspec_suffix, (
2563 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002564 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002565
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002566 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002567 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002568 print_stdout=True,
2569 # Flush after every line: useful for seeing progress when running as
2570 # recipe.
2571 filter_fn=lambda _: sys.stdout.flush())
2572
2573 if options.squash:
2574 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2575 change_numbers = [m.group(1)
2576 for m in map(regex.match, push_stdout.splitlines())
2577 if m]
2578 if len(change_numbers) != 1:
2579 DieWithError(
2580 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2581 'Change-Id: %s') % (len(change_numbers), change_id))
2582 self.SetIssue(change_numbers[0])
2583 RunGit(['config', 'branch.%s.gerritsquashhash' % self.GetBranch(),
2584 ref_to_push])
2585 return 0
2586
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002587 def _AddChangeIdToCommitMessage(self, options, args):
2588 """Re-commits using the current message, assumes the commit hook is in
2589 place.
2590 """
2591 log_desc = options.message or CreateDescriptionFromLog(args)
2592 git_command = ['commit', '--amend', '-m', log_desc]
2593 RunGit(git_command)
2594 new_log_desc = CreateDescriptionFromLog(args)
2595 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002596 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002597 return new_log_desc
2598 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002599 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002600
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002601 def SetCQState(self, new_state):
2602 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
2603 # TODO(tandrii): maybe allow configurability in codereview.settings or by
2604 # self-discovery of label config for this CL using REST API.
2605 vote_map = {
2606 _CQState.NONE: 0,
2607 _CQState.DRY_RUN: 1,
2608 _CQState.COMMIT : 2,
2609 }
2610 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2611 labels={'Commit-Queue': vote_map[new_state]})
2612
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002613
2614_CODEREVIEW_IMPLEMENTATIONS = {
2615 'rietveld': _RietveldChangelistImpl,
2616 'gerrit': _GerritChangelistImpl,
2617}
2618
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002619
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002620def _add_codereview_select_options(parser):
2621 """Appends --gerrit and --rietveld options to force specific codereview."""
2622 parser.codereview_group = optparse.OptionGroup(
2623 parser, 'EXPERIMENTAL! Codereview override options')
2624 parser.add_option_group(parser.codereview_group)
2625 parser.codereview_group.add_option(
2626 '--gerrit', action='store_true',
2627 help='Force the use of Gerrit for codereview')
2628 parser.codereview_group.add_option(
2629 '--rietveld', action='store_true',
2630 help='Force the use of Rietveld for codereview')
2631
2632
2633def _process_codereview_select_options(parser, options):
2634 if options.gerrit and options.rietveld:
2635 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2636 options.forced_codereview = None
2637 if options.gerrit:
2638 options.forced_codereview = 'gerrit'
2639 elif options.rietveld:
2640 options.forced_codereview = 'rietveld'
2641
2642
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002643class ChangeDescription(object):
2644 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002645 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002646 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002647
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002648 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002649 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002650
agable@chromium.org42c20792013-09-12 17:34:49 +00002651 @property # www.logilab.org/ticket/89786
2652 def description(self): # pylint: disable=E0202
2653 return '\n'.join(self._description_lines)
2654
2655 def set_description(self, desc):
2656 if isinstance(desc, basestring):
2657 lines = desc.splitlines()
2658 else:
2659 lines = [line.rstrip() for line in desc]
2660 while lines and not lines[0]:
2661 lines.pop(0)
2662 while lines and not lines[-1]:
2663 lines.pop(-1)
2664 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002665
piman@chromium.org336f9122014-09-04 02:16:55 +00002666 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002667 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002668 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002669 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002670 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002671 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002672
agable@chromium.org42c20792013-09-12 17:34:49 +00002673 # Get the set of R= and TBR= lines and remove them from the desciption.
2674 regexp = re.compile(self.R_LINE)
2675 matches = [regexp.match(line) for line in self._description_lines]
2676 new_desc = [l for i, l in enumerate(self._description_lines)
2677 if not matches[i]]
2678 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002679
agable@chromium.org42c20792013-09-12 17:34:49 +00002680 # Construct new unified R= and TBR= lines.
2681 r_names = []
2682 tbr_names = []
2683 for match in matches:
2684 if not match:
2685 continue
2686 people = cleanup_list([match.group(2).strip()])
2687 if match.group(1) == 'TBR':
2688 tbr_names.extend(people)
2689 else:
2690 r_names.extend(people)
2691 for name in r_names:
2692 if name not in reviewers:
2693 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002694 if add_owners_tbr:
2695 owners_db = owners.Database(change.RepositoryRoot(),
2696 fopen=file, os_path=os.path, glob=glob.glob)
2697 all_reviewers = set(tbr_names + reviewers)
2698 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2699 all_reviewers)
2700 tbr_names.extend(owners_db.reviewers_for(missing_files,
2701 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002702 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2703 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2704
2705 # Put the new lines in the description where the old first R= line was.
2706 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2707 if 0 <= line_loc < len(self._description_lines):
2708 if new_tbr_line:
2709 self._description_lines.insert(line_loc, new_tbr_line)
2710 if new_r_line:
2711 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002712 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002713 if new_r_line:
2714 self.append_footer(new_r_line)
2715 if new_tbr_line:
2716 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002717
2718 def prompt(self):
2719 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002720 self.set_description([
2721 '# Enter a description of the change.',
2722 '# This will be displayed on the codereview site.',
2723 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002724 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002725 '--------------------',
2726 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002727
agable@chromium.org42c20792013-09-12 17:34:49 +00002728 regexp = re.compile(self.BUG_LINE)
2729 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00002730 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00002731 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002732 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002733 if not content:
2734 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002735 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002736
2737 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002738 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2739 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002740 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002741 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002742
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002743 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00002744 """Adds a footer line to the description.
2745
2746 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
2747 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
2748 that Gerrit footers are always at the end.
2749 """
2750 parsed_footer_line = git_footers.parse_footer(line)
2751 if parsed_footer_line:
2752 # Line is a gerrit footer in the form: Footer-Key: any value.
2753 # Thus, must be appended observing Gerrit footer rules.
2754 self.set_description(
2755 git_footers.add_footer(self.description,
2756 key=parsed_footer_line[0],
2757 value=parsed_footer_line[1]))
2758 return
2759
2760 if not self._description_lines:
2761 self._description_lines.append(line)
2762 return
2763
2764 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
2765 if gerrit_footers:
2766 # git_footers.split_footers ensures that there is an empty line before
2767 # actual (gerrit) footers, if any. We have to keep it that way.
2768 assert top_lines and top_lines[-1] == ''
2769 top_lines, separator = top_lines[:-1], top_lines[-1:]
2770 else:
2771 separator = [] # No need for separator if there are no gerrit_footers.
2772
2773 prev_line = top_lines[-1] if top_lines else ''
2774 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
2775 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2776 top_lines.append('')
2777 top_lines.append(line)
2778 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002779
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002780 def get_reviewers(self):
2781 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002782 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
2783 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002784 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002785
2786
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002787def get_approving_reviewers(props):
2788 """Retrieves the reviewers that approved a CL from the issue properties with
2789 messages.
2790
2791 Note that the list may contain reviewers that are not committer, thus are not
2792 considered by the CQ.
2793 """
2794 return sorted(
2795 set(
2796 message['sender']
2797 for message in props['messages']
2798 if message['approval'] and message['sender'] in props['reviewers']
2799 )
2800 )
2801
2802
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002803def FindCodereviewSettingsFile(filename='codereview.settings'):
2804 """Finds the given file starting in the cwd and going up.
2805
2806 Only looks up to the top of the repository unless an
2807 'inherit-review-settings-ok' file exists in the root of the repository.
2808 """
2809 inherit_ok_file = 'inherit-review-settings-ok'
2810 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002811 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002812 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2813 root = '/'
2814 while True:
2815 if filename in os.listdir(cwd):
2816 if os.path.isfile(os.path.join(cwd, filename)):
2817 return open(os.path.join(cwd, filename))
2818 if cwd == root:
2819 break
2820 cwd = os.path.dirname(cwd)
2821
2822
2823def LoadCodereviewSettingsFromFile(fileobj):
2824 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002825 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002826
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002827 def SetProperty(name, setting, unset_error_ok=False):
2828 fullname = 'rietveld.' + name
2829 if setting in keyvals:
2830 RunGit(['config', fullname, keyvals[setting]])
2831 else:
2832 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2833
2834 SetProperty('server', 'CODE_REVIEW_SERVER')
2835 # Only server setting is required. Other settings can be absent.
2836 # In that case, we ignore errors raised during option deletion attempt.
2837 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002838 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002839 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2840 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002841 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002842 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002843 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2844 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002845 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002846 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002847 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002848 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2849 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002850
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002851 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002852 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002853
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002854 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07002855 RunGit(['config', 'gerrit.squash-uploads',
2856 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002857
tandrii@chromium.org28253532016-04-14 13:46:56 +00002858 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00002859 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00002860 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
2861
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002862 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2863 #should be of the form
2864 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2865 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2866 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2867 keyvals['ORIGIN_URL_CONFIG']])
2868
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002869
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002870def urlretrieve(source, destination):
2871 """urllib is broken for SSL connections via a proxy therefore we
2872 can't use urllib.urlretrieve()."""
2873 with open(destination, 'w') as f:
2874 f.write(urllib2.urlopen(source).read())
2875
2876
ukai@chromium.org712d6102013-11-27 00:52:58 +00002877def hasSheBang(fname):
2878 """Checks fname is a #! script."""
2879 with open(fname) as f:
2880 return f.read(2).startswith('#!')
2881
2882
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00002883# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
2884def DownloadHooks(*args, **kwargs):
2885 pass
2886
2887
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002888def DownloadGerritHook(force):
2889 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002890
2891 Args:
2892 force: True to update hooks. False to install hooks if not present.
2893 """
2894 if not settings.GetIsGerrit():
2895 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00002896 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002897 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2898 if not os.access(dst, os.X_OK):
2899 if os.path.exists(dst):
2900 if not force:
2901 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002902 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002903 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002904 if not hasSheBang(dst):
2905 DieWithError('Not a script: %s\n'
2906 'You need to download from\n%s\n'
2907 'into .git/hooks/commit-msg and '
2908 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002909 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2910 except Exception:
2911 if os.path.exists(dst):
2912 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002913 DieWithError('\nFailed to download hooks.\n'
2914 'You need to download from\n%s\n'
2915 'into .git/hooks/commit-msg and '
2916 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002917
2918
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002919
2920def GetRietveldCodereviewSettingsInteractively():
2921 """Prompt the user for settings."""
2922 server = settings.GetDefaultServerUrl(error_ok=True)
2923 prompt = 'Rietveld server (host[:port])'
2924 prompt += ' [%s]' % (server or DEFAULT_SERVER)
2925 newserver = ask_for_data(prompt + ':')
2926 if not server and not newserver:
2927 newserver = DEFAULT_SERVER
2928 if newserver:
2929 newserver = gclient_utils.UpgradeToHttps(newserver)
2930 if newserver != server:
2931 RunGit(['config', 'rietveld.server', newserver])
2932
2933 def SetProperty(initial, caption, name, is_url):
2934 prompt = caption
2935 if initial:
2936 prompt += ' ("x" to clear) [%s]' % initial
2937 new_val = ask_for_data(prompt + ':')
2938 if new_val == 'x':
2939 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
2940 elif new_val:
2941 if is_url:
2942 new_val = gclient_utils.UpgradeToHttps(new_val)
2943 if new_val != initial:
2944 RunGit(['config', 'rietveld.' + name, new_val])
2945
2946 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
2947 SetProperty(settings.GetDefaultPrivateFlag(),
2948 'Private flag (rietveld only)', 'private', False)
2949 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
2950 'tree-status-url', False)
2951 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
2952 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
2953 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
2954 'run-post-upload-hook', False)
2955
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002956@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002957def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002958 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002959
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002960 print('WARNING: git cl config works for Rietveld only.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002961 'For Gerrit, see http://crbug.com/603116.')
2962 # TODO(tandrii): add Gerrit support as part of http://crbug.com/603116.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00002963 parser.add_option('--activate-update', action='store_true',
2964 help='activate auto-updating [rietveld] section in '
2965 '.git/config')
2966 parser.add_option('--deactivate-update', action='store_true',
2967 help='deactivate auto-updating [rietveld] section in '
2968 '.git/config')
2969 options, args = parser.parse_args(args)
2970
2971 if options.deactivate_update:
2972 RunGit(['config', 'rietveld.autoupdate', 'false'])
2973 return
2974
2975 if options.activate_update:
2976 RunGit(['config', '--unset', 'rietveld.autoupdate'])
2977 return
2978
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002979 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002980 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002981 return 0
2982
2983 url = args[0]
2984 if not url.endswith('codereview.settings'):
2985 url = os.path.join(url, 'codereview.settings')
2986
2987 # Load code review settings and download hooks (if available).
2988 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
2989 return 0
2990
2991
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002992def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002993 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002994 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
2995 branch = ShortBranchName(branchref)
2996 _, args = parser.parse_args(args)
2997 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07002998 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002999 return RunGit(['config', 'branch.%s.base-url' % branch],
3000 error_ok=False).strip()
3001 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003002 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003003 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3004 error_ok=False).strip()
3005
3006
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003007def color_for_status(status):
3008 """Maps a Changelist status to color, for CMDstatus and other tools."""
3009 return {
3010 'unsent': Fore.RED,
3011 'waiting': Fore.BLUE,
3012 'reply': Fore.YELLOW,
3013 'lgtm': Fore.GREEN,
3014 'commit': Fore.MAGENTA,
3015 'closed': Fore.CYAN,
3016 'error': Fore.WHITE,
3017 }.get(status, Fore.WHITE)
3018
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003019
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003020def get_cl_statuses(changes, fine_grained, max_processes=None):
3021 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003022
3023 If fine_grained is true, this will fetch CL statuses from the server.
3024 Otherwise, simply indicate if there's a matching url for the given branches.
3025
3026 If max_processes is specified, it is used as the maximum number of processes
3027 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3028 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003029
3030 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003031 """
3032 # Silence upload.py otherwise it becomes unwieldly.
3033 upload.verbosity = 0
3034
3035 if fine_grained:
3036 # Process one branch synchronously to work through authentication, then
3037 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003038 if changes:
3039 fetch = lambda cl: (cl, cl.GetStatus())
3040 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003041
kmarshall3bff56b2016-06-06 18:31:47 -07003042 if not changes:
3043 # Exit early if there was only one branch to fetch.
3044 return
3045
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003046 changes_to_fetch = changes[1:]
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003047 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003048 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003049 if max_processes is not None
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003050 else len(changes_to_fetch))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003051
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003052 fetched_cls = set()
3053 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003054 while True:
3055 try:
3056 row = it.next(timeout=5)
3057 except multiprocessing.TimeoutError:
3058 break
3059
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003060 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003061 yield row
3062
3063 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003064 for cl in set(changes_to_fetch) - fetched_cls:
3065 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003066
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003067 else:
3068 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003069 for cl in changes:
3070 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003071
rmistry@google.com2dd99862015-06-22 12:22:18 +00003072
3073def upload_branch_deps(cl, args):
3074 """Uploads CLs of local branches that are dependents of the current branch.
3075
3076 If the local branch dependency tree looks like:
3077 test1 -> test2.1 -> test3.1
3078 -> test3.2
3079 -> test2.2 -> test3.3
3080
3081 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3082 run on the dependent branches in this order:
3083 test2.1, test3.1, test3.2, test2.2, test3.3
3084
3085 Note: This function does not rebase your local dependent branches. Use it when
3086 you make a change to the parent branch that will not conflict with its
3087 dependent branches, and you would like their dependencies updated in
3088 Rietveld.
3089 """
3090 if git_common.is_dirty_git_tree('upload-branch-deps'):
3091 return 1
3092
3093 root_branch = cl.GetBranch()
3094 if root_branch is None:
3095 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3096 'Get on a branch!')
3097 if not cl.GetIssue() or not cl.GetPatchset():
3098 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3099 'patchset dependencies without an uploaded CL.')
3100
3101 branches = RunGit(['for-each-ref',
3102 '--format=%(refname:short) %(upstream:short)',
3103 'refs/heads'])
3104 if not branches:
3105 print('No local branches found.')
3106 return 0
3107
3108 # Create a dictionary of all local branches to the branches that are dependent
3109 # on it.
3110 tracked_to_dependents = collections.defaultdict(list)
3111 for b in branches.splitlines():
3112 tokens = b.split()
3113 if len(tokens) == 2:
3114 branch_name, tracked = tokens
3115 tracked_to_dependents[tracked].append(branch_name)
3116
vapiera7fbd5a2016-06-16 09:17:49 -07003117 print()
3118 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003119 dependents = []
3120 def traverse_dependents_preorder(branch, padding=''):
3121 dependents_to_process = tracked_to_dependents.get(branch, [])
3122 padding += ' '
3123 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003124 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003125 dependents.append(dependent)
3126 traverse_dependents_preorder(dependent, padding)
3127 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003128 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003129
3130 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003131 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003132 return 0
3133
vapiera7fbd5a2016-06-16 09:17:49 -07003134 print('This command will checkout all dependent branches and run '
3135 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003136 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3137
andybons@chromium.org962f9462016-02-03 20:00:42 +00003138 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003139 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003140 args.extend(['-t', 'Updated patchset dependency'])
3141
rmistry@google.com2dd99862015-06-22 12:22:18 +00003142 # Record all dependents that failed to upload.
3143 failures = {}
3144 # Go through all dependents, checkout the branch and upload.
3145 try:
3146 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003147 print()
3148 print('--------------------------------------')
3149 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003150 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003151 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003152 try:
3153 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003154 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003155 failures[dependent_branch] = 1
3156 except: # pylint: disable=W0702
3157 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003158 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003159 finally:
3160 # Swap back to the original root branch.
3161 RunGit(['checkout', '-q', root_branch])
3162
vapiera7fbd5a2016-06-16 09:17:49 -07003163 print()
3164 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003165 for dependent_branch in dependents:
3166 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003167 print(' %s : %s' % (dependent_branch, upload_status))
3168 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003169
3170 return 0
3171
3172
kmarshall3bff56b2016-06-06 18:31:47 -07003173def CMDarchive(parser, args):
3174 """Archives and deletes branches associated with closed changelists."""
3175 parser.add_option(
3176 '-j', '--maxjobs', action='store', type=int,
3177 help='The maximum number of jobs to use when retrieving review status')
3178 parser.add_option(
3179 '-f', '--force', action='store_true',
3180 help='Bypasses the confirmation prompt.')
3181
3182 auth.add_auth_options(parser)
3183 options, args = parser.parse_args(args)
3184 if args:
3185 parser.error('Unsupported args: %s' % ' '.join(args))
3186 auth_config = auth.extract_auth_config_from_options(options)
3187
3188 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3189 if not branches:
3190 return 0
3191
vapiera7fbd5a2016-06-16 09:17:49 -07003192 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003193 changes = [Changelist(branchref=b, auth_config=auth_config)
3194 for b in branches.splitlines()]
3195 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3196 statuses = get_cl_statuses(changes,
3197 fine_grained=True,
3198 max_processes=options.maxjobs)
3199 proposal = [(cl.GetBranch(),
3200 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3201 for cl, status in statuses
3202 if status == 'closed']
3203 proposal.sort()
3204
3205 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003206 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003207 return 0
3208
3209 current_branch = GetCurrentBranch()
3210
vapiera7fbd5a2016-06-16 09:17:49 -07003211 print('\nBranches with closed issues that will be archived:\n')
3212 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
kmarshall3bff56b2016-06-06 18:31:47 -07003213 for next_item in proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003214 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003215
3216 if any(branch == current_branch for branch, _ in proposal):
3217 print('You are currently on a branch \'%s\' which is associated with a '
3218 'closed codereview issue, so archive cannot proceed. Please '
3219 'checkout another branch and run this command again.' %
3220 current_branch)
3221 return 1
3222
3223 if not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003224 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3225 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003226 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003227 return 1
3228
3229 for branch, tagname in proposal:
3230 RunGit(['tag', tagname, branch])
3231 RunGit(['branch', '-D', branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003232 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003233
3234 return 0
3235
3236
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003237def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003238 """Show status of changelists.
3239
3240 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003241 - Red not sent for review or broken
3242 - Blue waiting for review
3243 - Yellow waiting for you to reply to review
3244 - Green LGTM'ed
3245 - Magenta in the commit queue
3246 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003247
3248 Also see 'git cl comments'.
3249 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003250 parser.add_option('--field',
3251 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003252 parser.add_option('-f', '--fast', action='store_true',
3253 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003254 parser.add_option(
3255 '-j', '--maxjobs', action='store', type=int,
3256 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003257
3258 auth.add_auth_options(parser)
3259 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003260 if args:
3261 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003262 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003263
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003264 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003265 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003266 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003267 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003268 elif options.field == 'id':
3269 issueid = cl.GetIssue()
3270 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003271 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003272 elif options.field == 'patch':
3273 patchset = cl.GetPatchset()
3274 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003275 print(patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003276 elif options.field == 'url':
3277 url = cl.GetIssueURL()
3278 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003279 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003280 return 0
3281
3282 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3283 if not branches:
3284 print('No local branch found.')
3285 return 0
3286
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003287 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003288 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003289 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003290 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003291 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003292 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003293 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003294
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003295 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003296 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3297 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3298 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003299 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003300 c, status = output.next()
3301 branch_statuses[c.GetBranch()] = status
3302 status = branch_statuses.pop(branch)
3303 url = cl.GetIssueURL()
3304 if url and (not status or status == 'error'):
3305 # The issue probably doesn't exist anymore.
3306 url += ' (broken)'
3307
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003308 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003309 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003310 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003311 color = ''
3312 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003313 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003314 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003315 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003316 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003317
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003318 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003319 print()
3320 print('Current branch:',)
3321 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003322 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003323 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003324 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003325 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003326 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003327 print('Issue description:')
3328 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003329 return 0
3330
3331
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003332def colorize_CMDstatus_doc():
3333 """To be called once in main() to add colors to git cl status help."""
3334 colors = [i for i in dir(Fore) if i[0].isupper()]
3335
3336 def colorize_line(line):
3337 for color in colors:
3338 if color in line.upper():
3339 # Extract whitespaces first and the leading '-'.
3340 indent = len(line) - len(line.lstrip(' ')) + 1
3341 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3342 return line
3343
3344 lines = CMDstatus.__doc__.splitlines()
3345 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3346
3347
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003348@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003349def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003350 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003351
3352 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003353 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003354 parser.add_option('-r', '--reverse', action='store_true',
3355 help='Lookup the branch(es) for the specified issues. If '
3356 'no issues are specified, all branches with mapped '
3357 'issues will be listed.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003358 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003359 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003360 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003361
dnj@chromium.org406c4402015-03-03 17:22:28 +00003362 if options.reverse:
3363 branches = RunGit(['for-each-ref', 'refs/heads',
3364 '--format=%(refname:short)']).splitlines()
3365
3366 # Reverse issue lookup.
3367 issue_branch_map = {}
3368 for branch in branches:
3369 cl = Changelist(branchref=branch)
3370 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3371 if not args:
3372 args = sorted(issue_branch_map.iterkeys())
3373 for issue in args:
3374 if not issue:
3375 continue
vapiera7fbd5a2016-06-16 09:17:49 -07003376 print('Branch for issue number %s: %s' % (
3377 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
dnj@chromium.org406c4402015-03-03 17:22:28 +00003378 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003379 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003380 if len(args) > 0:
3381 try:
3382 issue = int(args[0])
3383 except ValueError:
3384 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003385 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003386 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003387 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003388 return 0
3389
3390
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003391def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003392 """Shows or posts review comments for any changelist."""
3393 parser.add_option('-a', '--add-comment', dest='comment',
3394 help='comment to add to an issue')
3395 parser.add_option('-i', dest='issue',
3396 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003397 parser.add_option('-j', '--json-file',
3398 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003399 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003400 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003401 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003402
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003403 issue = None
3404 if options.issue:
3405 try:
3406 issue = int(options.issue)
3407 except ValueError:
3408 DieWithError('A review issue id is expected to be a number')
3409
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003410 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003411
3412 if options.comment:
3413 cl.AddComment(options.comment)
3414 return 0
3415
3416 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003417 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003418 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003419 summary.append({
3420 'date': message['date'],
3421 'lgtm': False,
3422 'message': message['text'],
3423 'not_lgtm': False,
3424 'sender': message['sender'],
3425 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003426 if message['disapproval']:
3427 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003428 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003429 elif message['approval']:
3430 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003431 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003432 elif message['sender'] == data['owner_email']:
3433 color = Fore.MAGENTA
3434 else:
3435 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003436 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003437 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003438 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003439 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003440 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003441 if options.json_file:
3442 with open(options.json_file, 'wb') as f:
3443 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003444 return 0
3445
3446
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003447@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003448def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003449 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003450 parser.add_option('-d', '--display', action='store_true',
3451 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003452 parser.add_option('-n', '--new-description',
3453 help='New description to set for this issue (- for stdin)')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003454
3455 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003456 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003457 options, args = parser.parse_args(args)
3458 _process_codereview_select_options(parser, options)
3459
3460 target_issue = None
3461 if len(args) > 0:
3462 issue_arg = ParseIssueNumberArgument(args[0])
3463 if not issue_arg.valid:
3464 parser.print_help()
3465 return 1
3466 target_issue = issue_arg.issue
3467
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003468 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003469
3470 cl = Changelist(
3471 auth_config=auth_config, issue=target_issue,
3472 codereview=options.forced_codereview)
3473
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003474 if not cl.GetIssue():
3475 DieWithError('This branch has no associated changelist.')
3476 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003477
smut@google.com34fb6b12015-07-13 20:03:26 +00003478 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003479 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003480 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003481
3482 if options.new_description:
3483 text = options.new_description
3484 if text == '-':
3485 text = '\n'.join(l.rstrip() for l in sys.stdin)
3486
3487 description.set_description(text)
3488 else:
3489 description.prompt()
3490
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003491 if cl.GetDescription() != description.description:
3492 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003493 return 0
3494
3495
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003496def CreateDescriptionFromLog(args):
3497 """Pulls out the commit log to use as a base for the CL description."""
3498 log_args = []
3499 if len(args) == 1 and not args[0].endswith('.'):
3500 log_args = [args[0] + '..']
3501 elif len(args) == 1 and args[0].endswith('...'):
3502 log_args = [args[0][:-1]]
3503 elif len(args) == 2:
3504 log_args = [args[0] + '..' + args[1]]
3505 else:
3506 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003507 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003508
3509
thestig@chromium.org44202a22014-03-11 19:22:18 +00003510def CMDlint(parser, args):
3511 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003512 parser.add_option('--filter', action='append', metavar='-x,+y',
3513 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003514 auth.add_auth_options(parser)
3515 options, args = parser.parse_args(args)
3516 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003517
3518 # Access to a protected member _XX of a client class
3519 # pylint: disable=W0212
3520 try:
3521 import cpplint
3522 import cpplint_chromium
3523 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003524 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003525 return 1
3526
3527 # Change the current working directory before calling lint so that it
3528 # shows the correct base.
3529 previous_cwd = os.getcwd()
3530 os.chdir(settings.GetRoot())
3531 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003532 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003533 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3534 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003535 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003536 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003537 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003538
3539 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003540 command = args + files
3541 if options.filter:
3542 command = ['--filter=' + ','.join(options.filter)] + command
3543 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003544
3545 white_regex = re.compile(settings.GetLintRegex())
3546 black_regex = re.compile(settings.GetLintIgnoreRegex())
3547 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3548 for filename in filenames:
3549 if white_regex.match(filename):
3550 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07003551 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003552 else:
3553 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3554 extra_check_functions)
3555 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003556 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003557 finally:
3558 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07003559 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003560 if cpplint._cpplint_state.error_count != 0:
3561 return 1
3562 return 0
3563
3564
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003565def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003566 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003567 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003568 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003569 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003570 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003571 auth.add_auth_options(parser)
3572 options, args = parser.parse_args(args)
3573 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003574
sbc@chromium.org71437c02015-04-09 19:29:40 +00003575 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07003576 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003577 return 1
3578
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003579 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003580 if args:
3581 base_branch = args[0]
3582 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003583 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003584 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003585
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003586 cl.RunHook(
3587 committing=not options.upload,
3588 may_prompt=False,
3589 verbose=options.verbose,
3590 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003591 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003592
3593
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003594def GenerateGerritChangeId(message):
3595 """Returns Ixxxxxx...xxx change id.
3596
3597 Works the same way as
3598 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3599 but can be called on demand on all platforms.
3600
3601 The basic idea is to generate git hash of a state of the tree, original commit
3602 message, author/committer info and timestamps.
3603 """
3604 lines = []
3605 tree_hash = RunGitSilent(['write-tree'])
3606 lines.append('tree %s' % tree_hash.strip())
3607 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3608 if code == 0:
3609 lines.append('parent %s' % parent.strip())
3610 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3611 lines.append('author %s' % author.strip())
3612 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3613 lines.append('committer %s' % committer.strip())
3614 lines.append('')
3615 # Note: Gerrit's commit-hook actually cleans message of some lines and
3616 # whitespace. This code is not doing this, but it clearly won't decrease
3617 # entropy.
3618 lines.append(message)
3619 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3620 stdin='\n'.join(lines))
3621 return 'I%s' % change_hash.strip()
3622
3623
wittman@chromium.org455dc922015-01-26 20:15:50 +00003624def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3625 """Computes the remote branch ref to use for the CL.
3626
3627 Args:
3628 remote (str): The git remote for the CL.
3629 remote_branch (str): The git remote branch for the CL.
3630 target_branch (str): The target branch specified by the user.
3631 pending_prefix (str): The pending prefix from the settings.
3632 """
3633 if not (remote and remote_branch):
3634 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003635
wittman@chromium.org455dc922015-01-26 20:15:50 +00003636 if target_branch:
3637 # Cannonicalize branch references to the equivalent local full symbolic
3638 # refs, which are then translated into the remote full symbolic refs
3639 # below.
3640 if '/' not in target_branch:
3641 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3642 else:
3643 prefix_replacements = (
3644 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3645 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3646 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3647 )
3648 match = None
3649 for regex, replacement in prefix_replacements:
3650 match = re.search(regex, target_branch)
3651 if match:
3652 remote_branch = target_branch.replace(match.group(0), replacement)
3653 break
3654 if not match:
3655 # This is a branch path but not one we recognize; use as-is.
3656 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003657 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3658 # Handle the refs that need to land in different refs.
3659 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003660
wittman@chromium.org455dc922015-01-26 20:15:50 +00003661 # Create the true path to the remote branch.
3662 # Does the following translation:
3663 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3664 # * refs/remotes/origin/master -> refs/heads/master
3665 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3666 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3667 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3668 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3669 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3670 'refs/heads/')
3671 elif remote_branch.startswith('refs/remotes/branch-heads'):
3672 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3673 # If a pending prefix exists then replace refs/ with it.
3674 if pending_prefix:
3675 remote_branch = remote_branch.replace('refs/', pending_prefix)
3676 return remote_branch
3677
3678
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003679def cleanup_list(l):
3680 """Fixes a list so that comma separated items are put as individual items.
3681
3682 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3683 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3684 """
3685 items = sum((i.split(',') for i in l), [])
3686 stripped_items = (i.strip() for i in items)
3687 return sorted(filter(None, stripped_items))
3688
3689
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003690@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003691def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003692 """Uploads the current changelist to codereview.
3693
3694 Can skip dependency patchset uploads for a branch by running:
3695 git config branch.branch_name.skip-deps-uploads True
3696 To unset run:
3697 git config --unset branch.branch_name.skip-deps-uploads
3698 Can also set the above globally by using the --global flag.
3699 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003700 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3701 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003702 parser.add_option('--bypass-watchlists', action='store_true',
3703 dest='bypass_watchlists',
3704 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003705 parser.add_option('-f', action='store_true', dest='force',
3706 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003707 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003708 parser.add_option('-t', dest='title',
3709 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003710 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003711 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003712 help='reviewer email addresses')
3713 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003714 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003715 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003716 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003717 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003718 parser.add_option('--emulate_svn_auto_props',
3719 '--emulate-svn-auto-props',
3720 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003721 dest="emulate_svn_auto_props",
3722 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003723 parser.add_option('-c', '--use-commit-queue', action='store_true',
3724 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003725 parser.add_option('--private', action='store_true',
3726 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003727 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003728 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003729 metavar='TARGET',
3730 help='Apply CL to remote ref TARGET. ' +
3731 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003732 parser.add_option('--squash', action='store_true',
3733 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003734 parser.add_option('--no-squash', action='store_true',
3735 help='Don\'t squash multiple commits into one ' +
3736 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003737 parser.add_option('--email', default=None,
3738 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003739 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3740 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003741 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3742 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003743 help='Send the patchset to do a CQ dry run right after '
3744 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003745 parser.add_option('--dependencies', action='store_true',
3746 help='Uploads CLs of all the local branches that depend on '
3747 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003748
rmistry@google.com2dd99862015-06-22 12:22:18 +00003749 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003750 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003751 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003752 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003753 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003754 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003755 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003756
sbc@chromium.org71437c02015-04-09 19:29:40 +00003757 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003758 return 1
3759
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003760 options.reviewers = cleanup_list(options.reviewers)
3761 options.cc = cleanup_list(options.cc)
3762
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003763 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3764 settings.GetIsGerrit()
3765
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003766 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00003767 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003768
3769
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003770def IsSubmoduleMergeCommit(ref):
3771 # When submodules are added to the repo, we expect there to be a single
3772 # non-git-svn merge commit at remote HEAD with a signature comment.
3773 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003774 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003775 return RunGit(cmd) != ''
3776
3777
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003778def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003779 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003780
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003781 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3782 upstream and closes the issue automatically and atomically.
3783
3784 Otherwise (in case of Rietveld):
3785 Squashes branch into a single commit.
3786 Updates changelog with metadata (e.g. pointer to review).
3787 Pushes/dcommits the code upstream.
3788 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003789 """
3790 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3791 help='bypass upload presubmit hook')
3792 parser.add_option('-m', dest='message',
3793 help="override review description")
3794 parser.add_option('-f', action='store_true', dest='force',
3795 help="force yes to questions (don't prompt)")
3796 parser.add_option('-c', dest='contributor',
3797 help="external contributor for patch (appended to " +
3798 "description and used as author for git). Should be " +
3799 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003800 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003801 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003802 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003803 auth_config = auth.extract_auth_config_from_options(options)
3804
3805 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003806
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003807 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3808 if cl.IsGerrit():
3809 if options.message:
3810 # This could be implemented, but it requires sending a new patch to
3811 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3812 # Besides, Gerrit has the ability to change the commit message on submit
3813 # automatically, thus there is no need to support this option (so far?).
3814 parser.error('-m MESSAGE option is not supported for Gerrit.')
3815 if options.contributor:
3816 parser.error(
3817 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3818 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3819 'the contributor\'s "name <email>". If you can\'t upload such a '
3820 'commit for review, contact your repository admin and request'
3821 '"Forge-Author" permission.')
3822 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3823 options.verbose)
3824
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003825 current = cl.GetBranch()
3826 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3827 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07003828 print()
3829 print('Attempting to push branch %r into another local branch!' % current)
3830 print()
3831 print('Either reparent this branch on top of origin/master:')
3832 print(' git reparent-branch --root')
3833 print()
3834 print('OR run `git rebase-update` if you think the parent branch is ')
3835 print('already committed.')
3836 print()
3837 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003838 return 1
3839
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003840 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003841 # Default to merging against our best guess of the upstream branch.
3842 args = [cl.GetUpstreamBranch()]
3843
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003844 if options.contributor:
3845 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07003846 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003847 return 1
3848
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003849 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003850 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003851
sbc@chromium.org71437c02015-04-09 19:29:40 +00003852 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003853 return 1
3854
3855 # This rev-list syntax means "show all commits not in my branch that
3856 # are in base_branch".
3857 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3858 base_branch]).splitlines()
3859 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07003860 print('Base branch "%s" has %d commits '
3861 'not in this branch.' % (base_branch, len(upstream_commits)))
3862 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003863 return 1
3864
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003865 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003866 svn_head = None
3867 if cmd == 'dcommit' or base_has_submodules:
3868 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
3869 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003870
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003871 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003872 # If the base_head is a submodule merge commit, the first parent of the
3873 # base_head should be a git-svn commit, which is what we're interested in.
3874 base_svn_head = base_branch
3875 if base_has_submodules:
3876 base_svn_head += '^1'
3877
3878 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003879 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07003880 print('This branch has %d additional commits not upstreamed yet.'
3881 % len(extra_commits.splitlines()))
3882 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
3883 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003884 return 1
3885
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003886 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003887 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003888 author = None
3889 if options.contributor:
3890 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003891 hook_results = cl.RunHook(
3892 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003893 may_prompt=not options.force,
3894 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003895 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003896 if not hook_results.should_continue():
3897 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003898
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003899 # Check the tree status if the tree status URL is set.
3900 status = GetTreeStatus()
3901 if 'closed' == status:
3902 print('The tree is closed. Please wait for it to reopen. Use '
3903 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3904 return 1
3905 elif 'unknown' == status:
3906 print('Unable to determine tree status. Please verify manually and '
3907 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3908 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003909
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003910 change_desc = ChangeDescription(options.message)
3911 if not change_desc.description and cl.GetIssue():
3912 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003913
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003914 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00003915 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003916 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00003917 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003918 print('No description set.')
3919 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00003920 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003921
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003922 # Keep a separate copy for the commit message, because the commit message
3923 # contains the link to the Rietveld issue, while the Rietveld message contains
3924 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003925 # Keep a separate copy for the commit message.
3926 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00003927 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003928
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003929 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00003930 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00003931 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00003932 # after it. Add a period on a new line to circumvent this. Also add a space
3933 # before the period to make sure that Gitiles continues to correctly resolve
3934 # the URL.
3935 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003936 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003937 commit_desc.append_footer('Patch from %s.' % options.contributor)
3938
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00003939 print('Description:')
3940 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003941
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003942 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003943 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00003944 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003945
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003946 # We want to squash all this branch's commits into one commit with the proper
3947 # description. We do this by doing a "reset --soft" to the base branch (which
3948 # keeps the working copy the same), then dcommitting that. If origin/master
3949 # has a submodule merge commit, we'll also need to cherry-pick the squashed
3950 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003951 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003952 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
3953 # Delete the branches if they exist.
3954 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
3955 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
3956 result = RunGitWithCode(showref_cmd)
3957 if result[0] == 0:
3958 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003959
3960 # We might be in a directory that's present in this branch but not in the
3961 # trunk. Move up to the top of the tree so that git commands that expect a
3962 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003963 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003964 if rel_base_path:
3965 os.chdir(rel_base_path)
3966
3967 # Stuff our change into the merge branch.
3968 # We wrap in a try...finally block so if anything goes wrong,
3969 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003970 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003971 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003972 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003973 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003974 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00003975 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003976 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003977 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003978 RunGit(
3979 [
3980 'commit', '--author', options.contributor,
3981 '-m', commit_desc.description,
3982 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003983 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003984 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003985 if base_has_submodules:
3986 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
3987 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
3988 RunGit(['checkout', CHERRY_PICK_BRANCH])
3989 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003990 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003991 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003992 mirror = settings.GetGitMirror(remote)
3993 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003994 pending_prefix = settings.GetPendingRefPrefix()
3995 if not pending_prefix or branch.startswith(pending_prefix):
3996 # If not using refs/pending/heads/* at all, or target ref is already set
3997 # to pending, then push to the target ref directly.
3998 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003999 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004000 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004001 else:
4002 # Cherry-pick the change on top of pending ref and then push it.
4003 assert branch.startswith('refs/'), branch
4004 assert pending_prefix[-1] == '/', pending_prefix
4005 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004006 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004007 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004008 if retcode == 0:
4009 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004010 else:
4011 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004012 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00004013 'svn', 'dcommit',
4014 '-C%s' % options.similarity,
4015 '--no-rebase', '--rmdir',
4016 ]
4017 if settings.GetForceHttpsCommitUrl():
4018 # Allow forcing https commit URLs for some projects that don't allow
4019 # committing to http URLs (like Google Code).
4020 remote_url = cl.GetGitSvnRemoteUrl()
4021 if urlparse.urlparse(remote_url).scheme == 'http':
4022 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004023 cmd_args.append('--commit-url=%s' % remote_url)
4024 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004025 if 'Committed r' in output:
4026 revision = re.match(
4027 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4028 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004029 finally:
4030 # And then swap back to the original branch and clean up.
4031 RunGit(['checkout', '-q', cl.GetBranch()])
4032 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004033 if base_has_submodules:
4034 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004035
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004036 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004037 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004038 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004039
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004040 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004041 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004042 try:
4043 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4044 # We set pushed_to_pending to False, since it made it all the way to the
4045 # real ref.
4046 pushed_to_pending = False
4047 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004048 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004049
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004050 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004051 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004052 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004053 if not to_pending:
4054 if viewvc_url and revision:
4055 change_desc.append_footer(
4056 'Committed: %s%s' % (viewvc_url, revision))
4057 elif revision:
4058 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004059 print('Closing issue '
4060 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004061 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004062 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004063 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004064 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004065 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004066 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004067 if options.bypass_hooks:
4068 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4069 else:
4070 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004071 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004072 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004073
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004074 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004075 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004076 print('The commit is in the pending queue (%s).' % pending_ref)
4077 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4078 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004079
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004080 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4081 if os.path.isfile(hook):
4082 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004083
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004084 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004085
4086
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004087def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004088 print()
4089 print('Waiting for commit to be landed on %s...' % real_ref)
4090 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004091 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4092 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004093 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004094
4095 loop = 0
4096 while True:
4097 sys.stdout.write('fetching (%d)... \r' % loop)
4098 sys.stdout.flush()
4099 loop += 1
4100
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004101 if mirror:
4102 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004103 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4104 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4105 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4106 for commit in commits.splitlines():
4107 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004108 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004109 return commit
4110
4111 current_rev = to_rev
4112
4113
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004114def PushToGitPending(remote, pending_ref, upstream_ref):
4115 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4116
4117 Returns:
4118 (retcode of last operation, output log of last operation).
4119 """
4120 assert pending_ref.startswith('refs/'), pending_ref
4121 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4122 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4123 code = 0
4124 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004125 max_attempts = 3
4126 attempts_left = max_attempts
4127 while attempts_left:
4128 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004129 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004130 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004131
4132 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004133 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004134 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004135 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004136 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004137 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004138 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004139 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004140 continue
4141
4142 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004143 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004144 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004145 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004146 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004147 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4148 'the following files have merge conflicts:' % pending_ref)
4149 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4150 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004151 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004152 return code, out
4153
4154 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004155 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004156 code, out = RunGitWithCode(
4157 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4158 if code == 0:
4159 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004160 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004161 return code, out
4162
vapiera7fbd5a2016-06-16 09:17:49 -07004163 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004164 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004165 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004166 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004167 print('Fatal push error. Make sure your .netrc credentials and git '
4168 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004169 return code, out
4170
vapiera7fbd5a2016-06-16 09:17:49 -07004171 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004172 return code, out
4173
4174
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004175def IsFatalPushFailure(push_stdout):
4176 """True if retrying push won't help."""
4177 return '(prohibited by Gerrit)' in push_stdout
4178
4179
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004180@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004181def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004182 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004183 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004184 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004185 # If it looks like previous commits were mirrored with git-svn.
4186 message = """This repository appears to be a git-svn mirror, but no
4187upstream SVN master is set. You probably need to run 'git auto-svn' once."""
4188 else:
4189 message = """This doesn't appear to be an SVN repository.
4190If your project has a true, writeable git repository, you probably want to run
4191'git cl land' instead.
4192If your project has a git mirror of an upstream SVN master, you probably need
4193to run 'git svn init'.
4194
4195Using the wrong command might cause your commit to appear to succeed, and the
4196review to be closed, without actually landing upstream. If you choose to
4197proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004198 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004199 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
tandrii3bb82ff2016-06-17 07:36:36 -07004200 # TODO(tandrii): kill this post SVN migration with
4201 # https://codereview.chromium.org/2076683002
4202 print('WARNING: chrome infrastructure is migrating SVN repos to Git.\n'
4203 'Please let us know of this project you are committing to:'
4204 ' http://crbug.com/600451')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004205 return SendUpstream(parser, args, 'dcommit')
4206
4207
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004208@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004209def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004210 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004211 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004212 print('This appears to be an SVN repository.')
4213 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004214 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004215 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004216 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004217
4218
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004219@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004220def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004221 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004222 parser.add_option('-b', dest='newbranch',
4223 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004224 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004225 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004226 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4227 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004228 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004229 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004230 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004231 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004232 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004233 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004234
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004235
4236 group = optparse.OptionGroup(
4237 parser,
4238 'Options for continuing work on the current issue uploaded from a '
4239 'different clone (e.g. different machine). Must be used independently '
4240 'from the other options. No issue number should be specified, and the '
4241 'branch must have an issue number associated with it')
4242 group.add_option('--reapply', action='store_true', dest='reapply',
4243 help='Reset the branch and reapply the issue.\n'
4244 'CAUTION: This will undo any local changes in this '
4245 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004246
4247 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004248 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004249 parser.add_option_group(group)
4250
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004251 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004252 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004253 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004254 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004255 auth_config = auth.extract_auth_config_from_options(options)
4256
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004257
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004258 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004259 if options.newbranch:
4260 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004261 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004262 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004263
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004264 cl = Changelist(auth_config=auth_config,
4265 codereview=options.forced_codereview)
4266 if not cl.GetIssue():
4267 parser.error('current branch must have an associated issue')
4268
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004269 upstream = cl.GetUpstreamBranch()
4270 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004271 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004272
4273 RunGit(['reset', '--hard', upstream])
4274 if options.pull:
4275 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004276
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004277 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4278 options.directory)
4279
4280 if len(args) != 1 or not args[0]:
4281 parser.error('Must specify issue number or url')
4282
4283 # We don't want uncommitted changes mixed up with the patch.
4284 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004285 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004286
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004287 if options.newbranch:
4288 if options.force:
4289 RunGit(['branch', '-D', options.newbranch],
4290 stderr=subprocess2.PIPE, error_ok=True)
4291 RunGit(['new-branch', options.newbranch])
4292
4293 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4294
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004295 if cl.IsGerrit():
4296 if options.reject:
4297 parser.error('--reject is not supported with Gerrit codereview.')
4298 if options.nocommit:
4299 parser.error('--nocommit is not supported with Gerrit codereview.')
4300 if options.directory:
4301 parser.error('--directory is not supported with Gerrit codereview.')
4302
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004303 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004304 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004305
4306
4307def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004308 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004309 # Provide a wrapper for git svn rebase to help avoid accidental
4310 # git svn dcommit.
4311 # It's the only command that doesn't use parser at all since we just defer
4312 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004313
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004314 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004315
4316
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004317def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004318 """Fetches the tree status and returns either 'open', 'closed',
4319 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004320 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004321 if url:
4322 status = urllib2.urlopen(url).read().lower()
4323 if status.find('closed') != -1 or status == '0':
4324 return 'closed'
4325 elif status.find('open') != -1 or status == '1':
4326 return 'open'
4327 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004328 return 'unset'
4329
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004330
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004331def GetTreeStatusReason():
4332 """Fetches the tree status from a json url and returns the message
4333 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004334 url = settings.GetTreeStatusUrl()
4335 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004336 connection = urllib2.urlopen(json_url)
4337 status = json.loads(connection.read())
4338 connection.close()
4339 return status['message']
4340
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004341
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004342def GetBuilderMaster(bot_list):
4343 """For a given builder, fetch the master from AE if available."""
4344 map_url = 'https://builders-map.appspot.com/'
4345 try:
4346 master_map = json.load(urllib2.urlopen(map_url))
4347 except urllib2.URLError as e:
4348 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4349 (map_url, e))
4350 except ValueError as e:
4351 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4352 if not master_map:
4353 return None, 'Failed to build master map.'
4354
4355 result_master = ''
4356 for bot in bot_list:
4357 builder = bot.split(':', 1)[0]
4358 master_list = master_map.get(builder, [])
4359 if not master_list:
4360 return None, ('No matching master for builder %s.' % builder)
4361 elif len(master_list) > 1:
4362 return None, ('The builder name %s exists in multiple masters %s.' %
4363 (builder, master_list))
4364 else:
4365 cur_master = master_list[0]
4366 if not result_master:
4367 result_master = cur_master
4368 elif result_master != cur_master:
4369 return None, 'The builders do not belong to the same master.'
4370 return result_master, None
4371
4372
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004373def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004374 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004375 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004376 status = GetTreeStatus()
4377 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004378 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004379 return 2
4380
vapiera7fbd5a2016-06-16 09:17:49 -07004381 print('The tree is %s' % status)
4382 print()
4383 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004384 if status != 'open':
4385 return 1
4386 return 0
4387
4388
maruel@chromium.org15192402012-09-06 12:38:29 +00004389def CMDtry(parser, args):
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004390 """Triggers try jobs through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00004391 group = optparse.OptionGroup(parser, "Try job options")
4392 group.add_option(
4393 "-b", "--bot", action="append",
4394 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
4395 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004396 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00004397 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004398 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00004399 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004400 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00004401 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004402 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004403 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004404 "-r", "--revision",
4405 help="Revision to use for the try job; default: the "
4406 "revision will be determined by the try server; see "
4407 "its waterfall for more info")
4408 group.add_option(
4409 "-c", "--clobber", action="store_true", default=False,
4410 help="Force a clobber before building; e.g. don't do an "
4411 "incremental build")
4412 group.add_option(
4413 "--project",
4414 help="Override which project to use. Projects are defined "
4415 "server-side to define what default bot set to use")
4416 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004417 "-p", "--property", dest="properties", action="append", default=[],
4418 help="Specify generic properties in the form -p key1=value1 -p "
4419 "key2=value2 etc (buildbucket only). The value will be treated as "
4420 "json if decodable, or as string otherwise.")
4421 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004422 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004423 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004424 "--use-rietveld", action="store_true", default=False,
4425 help="Use Rietveld to trigger try jobs.")
4426 group.add_option(
4427 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4428 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004429 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004430 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004431 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004432 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004433
machenbach@chromium.org45453142015-09-15 08:45:22 +00004434 if options.use_rietveld and options.properties:
4435 parser.error('Properties can only be specified with buildbucket')
4436
4437 # Make sure that all properties are prop=value pairs.
4438 bad_params = [x for x in options.properties if '=' not in x]
4439 if bad_params:
4440 parser.error('Got properties with missing "=": %s' % bad_params)
4441
maruel@chromium.org15192402012-09-06 12:38:29 +00004442 if args:
4443 parser.error('Unknown arguments: %s' % args)
4444
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004445 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004446 if not cl.GetIssue():
4447 parser.error('Need to upload first')
4448
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004449 if cl.IsGerrit():
4450 parser.error(
4451 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4452 'If your project has Commit Queue, dry run is a workaround:\n'
4453 ' git cl set-commit --dry-run')
4454 # Code below assumes Rietveld issue.
4455 # TODO(tandrii): actually implement for Gerrit http://crbug.com/599931.
4456
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004457 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004458 if props.get('closed'):
4459 parser.error('Cannot send tryjobs for a closed CL')
4460
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004461 if props.get('private'):
4462 parser.error('Cannot use trybots with private issue')
4463
maruel@chromium.org15192402012-09-06 12:38:29 +00004464 if not options.name:
4465 options.name = cl.GetBranch()
4466
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004467 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004468 options.master, err_msg = GetBuilderMaster(options.bot)
4469 if err_msg:
4470 parser.error('Tryserver master cannot be found because: %s\n'
4471 'Please manually specify the tryserver master'
4472 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004473
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004474 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004475 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004476 if not options.bot:
4477 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004478
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004479 # Get try masters from PRESUBMIT.py files.
4480 masters = presubmit_support.DoGetTryMasters(
4481 change,
4482 change.LocalPaths(),
4483 settings.GetRoot(),
4484 None,
4485 None,
4486 options.verbose,
4487 sys.stdout)
4488 if masters:
4489 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004490
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004491 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4492 options.bot = presubmit_support.DoGetTrySlaves(
4493 change,
4494 change.LocalPaths(),
4495 settings.GetRoot(),
4496 None,
4497 None,
4498 options.verbose,
4499 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004500
4501 if not options.bot:
4502 # Get try masters from cq.cfg if any.
4503 # TODO(tandrii): some (but very few) projects store cq.cfg in different
4504 # location.
4505 cq_cfg = os.path.join(change.RepositoryRoot(),
4506 'infra', 'config', 'cq.cfg')
4507 if os.path.exists(cq_cfg):
4508 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00004509 cq_masters = commit_queue.get_master_builder_map(
4510 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004511 for master, builders in cq_masters.iteritems():
4512 for builder in builders:
4513 # Skip presubmit builders, because these will fail without LGTM.
machenbach@chromium.org2403e802016-04-29 12:34:42 +00004514 masters.setdefault(master, {})[builder] = ['defaulttests']
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004515 if masters:
tandriib93dd2b2016-06-07 08:03:08 -07004516 print('Loaded default bots from CQ config (%s)' % cq_cfg)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004517 return masters
tandriib93dd2b2016-06-07 08:03:08 -07004518 else:
4519 print('CQ config exists (%s) but has no try bots listed' % cq_cfg)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004520
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004521 if not options.bot:
4522 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00004523
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004524 builders_and_tests = {}
4525 # TODO(machenbach): The old style command-line options don't support
4526 # multiple try masters yet.
4527 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4528 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4529
4530 for bot in old_style:
4531 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004532 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004533 elif ',' in bot:
4534 parser.error('Specify one bot per --bot flag')
4535 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004536 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004537
4538 for bot, tests in new_style:
4539 builders_and_tests.setdefault(bot, []).extend(tests)
4540
4541 # Return a master map with one master to be backwards compatible. The
4542 # master name defaults to an empty string, which will cause the master
4543 # not to be set on rietveld (deprecated).
4544 return {options.master: builders_and_tests}
4545
4546 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004547
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004548 for builders in masters.itervalues():
4549 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004550 print('ERROR You are trying to send a job to a triggered bot. This type '
4551 'of bot requires an\ninitial job from a parent (usually a builder).'
4552 ' Instead send your job to the parent.\n'
4553 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004554 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004555
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004556 patchset = cl.GetMostRecentPatchset()
4557 if patchset and patchset != cl.GetPatchset():
4558 print(
4559 '\nWARNING Mismatch between local config and server. Did a previous '
4560 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4561 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004562 if options.luci:
4563 trigger_luci_job(cl, masters, options)
4564 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004565 try:
4566 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4567 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004568 print('ERROR: %s' % ex)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004569 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004570 except Exception as e:
4571 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
vapiera7fbd5a2016-06-16 09:17:49 -07004572 print('ERROR: Exception when trying to trigger tryjobs: %s\n%s' %
4573 (e, stacktrace))
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004574 return 1
4575 else:
4576 try:
4577 cl.RpcServer().trigger_distributed_try_jobs(
4578 cl.GetIssue(), patchset, options.name, options.clobber,
4579 options.revision, masters)
4580 except urllib2.HTTPError as e:
4581 if e.code == 404:
4582 print('404 from rietveld; '
4583 'did you mean to use "git try" instead of "git cl try"?')
4584 return 1
4585 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004586
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004587 for (master, builders) in sorted(masters.iteritems()):
4588 if master:
vapiera7fbd5a2016-06-16 09:17:49 -07004589 print('Master: %s' % master)
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004590 length = max(len(builder) for builder in builders)
4591 for builder in sorted(builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004592 print(' %*s: %s' % (length, builder, ','.join(builders[builder])))
maruel@chromium.org15192402012-09-06 12:38:29 +00004593 return 0
4594
4595
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004596def CMDtry_results(parser, args):
4597 group = optparse.OptionGroup(parser, "Try job results options")
4598 group.add_option(
4599 "-p", "--patchset", type=int, help="patchset number if not current.")
4600 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004601 "--print-master", action='store_true', help="print master name as well.")
4602 group.add_option(
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004603 "--color", action='store_true', default=setup_color.IS_TTY,
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004604 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004605 group.add_option(
4606 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4607 help="Host of buildbucket. The default host is %default.")
4608 parser.add_option_group(group)
4609 auth.add_auth_options(parser)
4610 options, args = parser.parse_args(args)
4611 if args:
4612 parser.error('Unrecognized args: %s' % ' '.join(args))
4613
4614 auth_config = auth.extract_auth_config_from_options(options)
4615 cl = Changelist(auth_config=auth_config)
4616 if not cl.GetIssue():
4617 parser.error('Need to upload first')
4618
4619 if not options.patchset:
4620 options.patchset = cl.GetMostRecentPatchset()
4621 if options.patchset and options.patchset != cl.GetPatchset():
4622 print(
4623 '\nWARNING Mismatch between local config and server. Did a previous '
4624 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4625 'Continuing using\npatchset %s.\n' % options.patchset)
4626 try:
4627 jobs = fetch_try_jobs(auth_config, cl, options)
4628 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004629 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004630 return 1
4631 except Exception as e:
4632 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
vapiera7fbd5a2016-06-16 09:17:49 -07004633 print('ERROR: Exception when trying to fetch tryjobs: %s\n%s' %
4634 (e, stacktrace))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004635 return 1
4636 print_tryjobs(options, jobs)
4637 return 0
4638
4639
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004640@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004641def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004642 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004643 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004644 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004645 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004646
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004647 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004648 if args:
4649 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004650 branch = cl.GetBranch()
4651 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004652 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004653 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004654
4655 # Clear configured merge-base, if there is one.
4656 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004657 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004658 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004659 return 0
4660
4661
thestig@chromium.org00858c82013-12-02 23:08:03 +00004662def CMDweb(parser, args):
4663 """Opens the current CL in the web browser."""
4664 _, args = parser.parse_args(args)
4665 if args:
4666 parser.error('Unrecognized args: %s' % ' '.join(args))
4667
4668 issue_url = Changelist().GetIssueURL()
4669 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004670 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004671 return 1
4672
4673 webbrowser.open(issue_url)
4674 return 0
4675
4676
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004677def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004678 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004679 parser.add_option('-d', '--dry-run', action='store_true',
4680 help='trigger in dry run mode')
4681 parser.add_option('-c', '--clear', action='store_true',
4682 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004683 auth.add_auth_options(parser)
4684 options, args = parser.parse_args(args)
4685 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004686 if args:
4687 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004688 if options.dry_run and options.clear:
4689 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4690
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004691 cl = Changelist(auth_config=auth_config)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004692 if options.clear:
4693 state = _CQState.CLEAR
4694 elif options.dry_run:
4695 state = _CQState.DRY_RUN
4696 else:
4697 state = _CQState.COMMIT
4698 if not cl.GetIssue():
4699 parser.error('Must upload the issue first')
4700 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004701 return 0
4702
4703
groby@chromium.org411034a2013-02-26 15:12:01 +00004704def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004705 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004706 auth.add_auth_options(parser)
4707 options, args = parser.parse_args(args)
4708 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004709 if args:
4710 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004711 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00004712 # Ensure there actually is an issue to close.
4713 cl.GetDescription()
4714 cl.CloseIssue()
4715 return 0
4716
4717
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004718def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004719 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004720 auth.add_auth_options(parser)
4721 options, args = parser.parse_args(args)
4722 auth_config = auth.extract_auth_config_from_options(options)
4723 if args:
4724 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004725
4726 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004727 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004728 # Staged changes would be committed along with the patch from last
4729 # upload, hence counted toward the "last upload" side in the final
4730 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004731 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004732 return 1
4733
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004734 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004735 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004736 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004737 if not issue:
4738 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004739 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004740 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004741
4742 # Create a new branch based on the merge-base
4743 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004744 # Clear cached branch in cl object, to avoid overwriting original CL branch
4745 # properties.
4746 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004747 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004748 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004749 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004750 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004751 return rtn
4752
wychen@chromium.org06928532015-02-03 02:11:29 +00004753 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004754 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00004755 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004756 finally:
4757 RunGit(['checkout', '-q', branch])
4758 RunGit(['branch', '-D', TMP_BRANCH])
4759
4760 return 0
4761
4762
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004763def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004764 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004765 parser.add_option(
4766 '--no-color',
4767 action='store_true',
4768 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004769 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004770 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004771 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004772
4773 author = RunGit(['config', 'user.email']).strip() or None
4774
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004775 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004776
4777 if args:
4778 if len(args) > 1:
4779 parser.error('Unknown args')
4780 base_branch = args[0]
4781 else:
4782 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004783 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004784
4785 change = cl.GetChange(base_branch, None)
4786 return owners_finder.OwnersFinder(
4787 [f.LocalPath() for f in
4788 cl.GetChange(base_branch, None).AffectedFiles()],
4789 change.RepositoryRoot(), author,
4790 fopen=file, os_path=os.path, glob=glob.glob,
4791 disable_color=options.no_color).run()
4792
4793
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004794def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004795 """Generates a diff command."""
4796 # Generate diff for the current branch's changes.
4797 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4798 upstream_commit, '--' ]
4799
4800 if args:
4801 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004802 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004803 diff_cmd.append(arg)
4804 else:
4805 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004806
4807 return diff_cmd
4808
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004809def MatchingFileType(file_name, extensions):
4810 """Returns true if the file name ends with one of the given extensions."""
4811 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004812
enne@chromium.org555cfe42014-01-29 18:21:39 +00004813@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004814def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004815 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00004816 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07004817 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004818 parser.add_option('--full', action='store_true',
4819 help='Reformat the full content of all touched files')
4820 parser.add_option('--dry-run', action='store_true',
4821 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004822 parser.add_option('--python', action='store_true',
4823 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004824 parser.add_option('--diff', action='store_true',
4825 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004826 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004827
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004828 # git diff generates paths against the root of the repository. Change
4829 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004830 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004831 if rel_base_path:
4832 os.chdir(rel_base_path)
4833
digit@chromium.org29e47272013-05-17 17:01:46 +00004834 # Grab the merge-base commit, i.e. the upstream commit of the current
4835 # branch when it was created or the last time it was rebased. This is
4836 # to cover the case where the user may have called "git fetch origin",
4837 # moving the origin branch to a newer commit, but hasn't rebased yet.
4838 upstream_commit = None
4839 cl = Changelist()
4840 upstream_branch = cl.GetUpstreamBranch()
4841 if upstream_branch:
4842 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4843 upstream_commit = upstream_commit.strip()
4844
4845 if not upstream_commit:
4846 DieWithError('Could not find base commit for this branch. '
4847 'Are you in detached state?')
4848
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004849 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4850 diff_output = RunGit(changed_files_cmd)
4851 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004852 # Filter out files deleted by this CL
4853 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004854
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004855 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4856 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4857 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004858 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004859
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004860 top_dir = os.path.normpath(
4861 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
4862
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004863 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4864 # formatted. This is used to block during the presubmit.
4865 return_value = 0
4866
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004867 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004868 # Locate the clang-format binary in the checkout
4869 try:
4870 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07004871 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004872 DieWithError(e)
4873
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004874 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004875 cmd = [clang_format_tool]
4876 if not opts.dry_run and not opts.diff:
4877 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004878 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004879 if opts.diff:
4880 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004881 else:
4882 env = os.environ.copy()
4883 env['PATH'] = str(os.path.dirname(clang_format_tool))
4884 try:
4885 script = clang_format.FindClangFormatScriptInChromiumTree(
4886 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07004887 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004888 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004889
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004890 cmd = [sys.executable, script, '-p0']
4891 if not opts.dry_run and not opts.diff:
4892 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004893
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004894 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
4895 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004896
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004897 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
4898 if opts.diff:
4899 sys.stdout.write(stdout)
4900 if opts.dry_run and len(stdout) > 0:
4901 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004902
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004903 # Similar code to above, but using yapf on .py files rather than clang-format
4904 # on C/C++ files
4905 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004906 yapf_tool = gclient_utils.FindExecutable('yapf')
4907 if yapf_tool is None:
4908 DieWithError('yapf not found in PATH')
4909
4910 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004911 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004912 cmd = [yapf_tool]
4913 if not opts.dry_run and not opts.diff:
4914 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004915 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004916 if opts.diff:
4917 sys.stdout.write(stdout)
4918 else:
4919 # TODO(sbc): yapf --lines mode still has some issues.
4920 # https://github.com/google/yapf/issues/154
4921 DieWithError('--python currently only works with --full')
4922
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004923 # Dart's formatter does not have the nice property of only operating on
4924 # modified chunks, so hard code full.
4925 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004926 try:
4927 command = [dart_format.FindDartFmtToolInChromiumTree()]
4928 if not opts.dry_run and not opts.diff:
4929 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004930 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004931
ppi@chromium.org6593d932016-03-03 15:41:15 +00004932 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004933 if opts.dry_run and stdout:
4934 return_value = 2
4935 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07004936 print('Warning: Unable to check Dart code formatting. Dart SDK not '
4937 'found in this checkout. Files in other languages are still '
4938 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004939
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004940 # Format GN build files. Always run on full build files for canonical form.
4941 if gn_diff_files:
4942 cmd = ['gn', 'format']
4943 if not opts.dry_run and not opts.diff:
4944 cmd.append('--in-place')
4945 for gn_diff_file in gn_diff_files:
bsep@chromium.org627d9002016-04-29 00:00:52 +00004946 stdout = RunCommand(cmd + [gn_diff_file],
4947 shell=sys.platform == 'win32',
4948 cwd=top_dir)
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004949 if opts.diff:
4950 sys.stdout.write(stdout)
4951
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004952 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004953
4954
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004955@subcommand.usage('<codereview url or issue id>')
4956def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004957 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004958 _, args = parser.parse_args(args)
4959
4960 if len(args) != 1:
4961 parser.print_help()
4962 return 1
4963
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004964 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00004965 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004966 parser.print_help()
4967 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00004968 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004969
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004970 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00004971 output = RunGit(['config', '--local', '--get-regexp',
4972 r'branch\..*\.%s' % issueprefix],
4973 error_ok=True)
4974 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004975 if issue == target_issue:
4976 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004977
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004978 branches = []
4979 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00004980 branches.extend(find_issues(cls.IssueSettingSuffix()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004981 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07004982 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004983 return 1
4984 if len(branches) == 1:
4985 RunGit(['checkout', branches[0]])
4986 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004987 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004988 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07004989 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004990 which = raw_input('Choose by index: ')
4991 try:
4992 RunGit(['checkout', branches[int(which)]])
4993 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07004994 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004995 return 1
4996
4997 return 0
4998
4999
maruel@chromium.org29404b52014-09-08 22:58:00 +00005000def CMDlol(parser, args):
5001 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005002 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005003 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5004 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5005 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005006 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005007 return 0
5008
5009
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005010class OptionParser(optparse.OptionParser):
5011 """Creates the option parse and add --verbose support."""
5012 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005013 optparse.OptionParser.__init__(
5014 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005015 self.add_option(
5016 '-v', '--verbose', action='count', default=0,
5017 help='Use 2 times for more debugging info')
5018
5019 def parse_args(self, args=None, values=None):
5020 options, args = optparse.OptionParser.parse_args(self, args, values)
5021 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5022 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5023 return options, args
5024
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005025
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005026def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005027 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005028 print('\nYour python version %s is unsupported, please upgrade.\n' %
5029 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005030 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005031
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005032 # Reload settings.
5033 global settings
5034 settings = Settings()
5035
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005036 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005037 dispatcher = subcommand.CommandDispatcher(__name__)
5038 try:
5039 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005040 except auth.AuthenticationError as e:
5041 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005042 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005043 if e.code != 500:
5044 raise
5045 DieWithError(
5046 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5047 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005048 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005049
5050
5051if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005052 # These affect sys.stdout so do it outside of main() to simplify mocks in
5053 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005054 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005055 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005056 try:
5057 sys.exit(main(sys.argv[1:]))
5058 except KeyboardInterrupt:
5059 sys.stderr.write('interrupted\n')
5060 sys.exit(1)