blob: 75be64e6780b0b963f3c0bfee8a5aa5200f2af9a [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00008"""A git-command for integrating reviews on Rietveld and Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
vapiera7fbd5a2016-06-16 09:17:49 -070010from __future__ import print_function
11
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000012from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000013from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000014import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000015import collections
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000016import glob
sheyang@google.com6ebaf782015-05-12 19:17:54 +000017import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000018import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000020import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import optparse
22import os
23import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000024import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000026import textwrap
sheyang@google.com6ebaf782015-05-12 19:17:54 +000027import time
28import traceback
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000029import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000030import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000031import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000032import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000033import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000034import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000035
36try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000037 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000038except ImportError:
39 pass
40
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000041from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000042from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000043from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000044import auth
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +000045from luci_hacks import trigger_luci_job as luci_trigger
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000046import clang_format
tandrii@chromium.org71184c02016-01-13 15:18:44 +000047import commit_queue
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000048import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000049import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000050import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000051import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000052import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000053import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000054import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000055import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000056import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000057import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000058import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000059import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000060import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000061import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000062import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000063import watchlists
64
maruel@chromium.org0633fb42013-08-16 20:06:14 +000065__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000066
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000067DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000068POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000069DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000070GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
rmistry@google.comc68112d2015-03-03 12:48:06 +000071REFS_THAT_ALIAS_TO_OTHER_REFS = {
72 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
73 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
74}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000075
thestig@chromium.org44202a22014-03-11 19:22:18 +000076# Valid extensions for files we want to lint.
77DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
78DEFAULT_LINT_IGNORE_REGEX = r"$^"
79
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000080# Shortcut since it quickly becomes redundant.
81Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000082
maruel@chromium.orgddd59412011-11-30 14:20:38 +000083# Initialized in main()
84settings = None
85
86
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000087def DieWithError(message):
vapiera7fbd5a2016-06-16 09:17:49 -070088 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000089 sys.exit(1)
90
91
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000092def GetNoGitPagerEnv():
93 env = os.environ.copy()
94 # 'cat' is a magical git string that disables pagers on all platforms.
95 env['GIT_PAGER'] = 'cat'
96 return env
97
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000098
bsep@chromium.org627d9002016-04-29 00:00:52 +000099def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000100 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000101 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000102 except subprocess2.CalledProcessError as e:
103 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000104 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000105 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000106 'Command "%s" failed.\n%s' % (
107 ' '.join(args), error_message or e.stdout or ''))
108 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000109
110
111def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000112 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000113 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000114
115
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000116def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000117 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000118 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000119 if suppress_stderr:
120 stderr = subprocess2.VOID
121 else:
122 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000123 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000124 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000125 stdout=subprocess2.PIPE,
126 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000127 return code, out[0]
128 except ValueError:
129 # When the subprocess fails, it returns None. That triggers a ValueError
130 # when trying to unpack the return value into (out, code).
131 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000132
133
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000134def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000135 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000136 return RunGitWithCode(args, suppress_stderr=True)[1]
137
138
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000139def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000140 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000141 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000142 return (version.startswith(prefix) and
143 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000144
145
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000146def BranchExists(branch):
147 """Return True if specified branch exists."""
148 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
149 suppress_stderr=True)
150 return not code
151
152
maruel@chromium.org90541732011-04-01 17:54:18 +0000153def ask_for_data(prompt):
154 try:
155 return raw_input(prompt)
156 except KeyboardInterrupt:
157 # Hide the exception.
158 sys.exit(1)
159
160
iannucci@chromium.org79540052012-10-19 23:15:26 +0000161def git_set_branch_value(key, value):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000162 branch = GetCurrentBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000163 if not branch:
164 return
165
166 cmd = ['config']
167 if isinstance(value, int):
168 cmd.append('--int')
169 git_key = 'branch.%s.%s' % (branch, key)
170 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000171
172
173def git_get_branch_default(key, default):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000174 branch = GetCurrentBranch()
iannucci@chromium.org79540052012-10-19 23:15:26 +0000175 if branch:
176 git_key = 'branch.%s.%s' % (branch, key)
177 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
178 try:
179 return int(stdout.strip())
180 except ValueError:
181 pass
182 return default
183
184
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000185def add_git_similarity(parser):
186 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000187 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000188 help='Sets the percentage that a pair of files need to match in order to'
189 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000190 parser.add_option(
191 '--find-copies', action='store_true',
192 help='Allows git to look for copies.')
193 parser.add_option(
194 '--no-find-copies', action='store_false', dest='find_copies',
195 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000196
197 old_parser_args = parser.parse_args
198 def Parse(args):
199 options, args = old_parser_args(args)
200
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000201 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000202 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000203 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000204 print('Note: Saving similarity of %d%% in git config.'
205 % options.similarity)
206 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000207
iannucci@chromium.org79540052012-10-19 23:15:26 +0000208 options.similarity = max(0, min(options.similarity, 100))
209
210 if options.find_copies is None:
211 options.find_copies = bool(
212 git_get_branch_default('git-find-copies', True))
213 else:
214 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000215
216 print('Using %d%% similarity for rename/copy detection. '
217 'Override with --similarity.' % options.similarity)
218
219 return options, args
220 parser.parse_args = Parse
221
222
machenbach@chromium.org45453142015-09-15 08:45:22 +0000223def _get_properties_from_options(options):
224 properties = dict(x.split('=', 1) for x in options.properties)
225 for key, val in properties.iteritems():
226 try:
227 properties[key] = json.loads(val)
228 except ValueError:
229 pass # If a value couldn't be evaluated, treat it as a string.
230 return properties
231
232
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000233def _prefix_master(master):
234 """Convert user-specified master name to full master name.
235
236 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
237 name, while the developers always use shortened master name
238 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
239 function does the conversion for buildbucket migration.
240 """
241 prefix = 'master.'
242 if master.startswith(prefix):
243 return master
244 return '%s%s' % (prefix, master)
245
246
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000247def _buildbucket_retry(operation_name, http, *args, **kwargs):
248 """Retries requests to buildbucket service and returns parsed json content."""
249 try_count = 0
250 while True:
251 response, content = http.request(*args, **kwargs)
252 try:
253 content_json = json.loads(content)
254 except ValueError:
255 content_json = None
256
257 # Buildbucket could return an error even if status==200.
258 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000259 error = content_json.get('error')
260 if error.get('code') == 403:
261 raise BuildbucketResponseException(
262 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000263 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000264 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000265 raise BuildbucketResponseException(msg)
266
267 if response.status == 200:
268 if not content_json:
269 raise BuildbucketResponseException(
270 'Buildbucket returns invalid json content: %s.\n'
271 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
272 content)
273 return content_json
274 if response.status < 500 or try_count >= 2:
275 raise httplib2.HttpLib2Error(content)
276
277 # status >= 500 means transient failures.
278 logging.debug('Transient errors when %s. Will retry.', operation_name)
279 time.sleep(0.5 + 1.5*try_count)
280 try_count += 1
281 assert False, 'unreachable'
282
283
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000284def trigger_luci_job(changelist, masters, options):
285 """Send a job to run on LUCI."""
286 issue_props = changelist.GetIssueProperties()
287 issue = changelist.GetIssue()
288 patchset = changelist.GetMostRecentPatchset()
289 for builders_and_tests in sorted(masters.itervalues()):
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000290 # TODO(hinoka et al): add support for other properties.
291 # Currently, this completely ignores testfilter and other properties.
292 for builder in sorted(builders_and_tests):
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000293 luci_trigger.trigger(
294 builder, 'HEAD', issue, patchset, issue_props['project'])
295
296
machenbach@chromium.org45453142015-09-15 08:45:22 +0000297def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000298 rietveld_url = settings.GetDefaultServerUrl()
299 rietveld_host = urlparse.urlparse(rietveld_url).hostname
300 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
301 http = authenticator.authorize(httplib2.Http())
302 http.force_exception_to_status_code = True
303 issue_props = changelist.GetIssueProperties()
304 issue = changelist.GetIssue()
305 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000306 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000307
308 buildbucket_put_url = (
309 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000310 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000311 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
312 hostname=rietveld_host,
313 issue=issue,
314 patch=patchset)
315
316 batch_req_body = {'builds': []}
317 print_text = []
318 print_text.append('Tried jobs on:')
319 for master, builders_and_tests in sorted(masters.iteritems()):
320 print_text.append('Master: %s' % master)
321 bucket = _prefix_master(master)
322 for builder, tests in sorted(builders_and_tests.iteritems()):
323 print_text.append(' %s: %s' % (builder, tests))
324 parameters = {
325 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000326 'changes': [{
327 'author': {'email': issue_props['owner_email']},
328 'revision': options.revision,
329 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000330 'properties': {
331 'category': category,
332 'issue': issue,
333 'master': master,
334 'patch_project': issue_props['project'],
335 'patch_storage': 'rietveld',
336 'patchset': patchset,
337 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000338 'rietveld': rietveld_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000339 },
340 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000341 if 'presubmit' in builder.lower():
342 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000343 if tests:
344 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000345 if properties:
346 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000347 if options.clobber:
348 parameters['properties']['clobber'] = True
349 batch_req_body['builds'].append(
350 {
351 'bucket': bucket,
352 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000353 'client_operation_id': str(uuid.uuid4()),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000354 'tags': ['builder:%s' % builder,
355 'buildset:%s' % buildset,
356 'master:%s' % master,
357 'user_agent:git_cl_try']
358 }
359 )
360
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000361 _buildbucket_retry(
362 'triggering tryjobs',
363 http,
364 buildbucket_put_url,
365 'PUT',
366 body=json.dumps(batch_req_body),
367 headers={'Content-Type': 'application/json'}
368 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000369 print_text.append('To see results here, run: git cl try-results')
370 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700371 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000372
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000373
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000374def fetch_try_jobs(auth_config, changelist, options):
375 """Fetches tryjobs from buildbucket.
376
377 Returns a map from build id to build info as json dictionary.
378 """
379 rietveld_url = settings.GetDefaultServerUrl()
380 rietveld_host = urlparse.urlparse(rietveld_url).hostname
381 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
382 if authenticator.has_cached_credentials():
383 http = authenticator.authorize(httplib2.Http())
384 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700385 print('Warning: Some results might be missing because %s' %
386 # Get the message on how to login.
387 (auth.LoginRequiredError(rietveld_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000388 http = httplib2.Http()
389
390 http.force_exception_to_status_code = True
391
392 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
393 hostname=rietveld_host,
394 issue=changelist.GetIssue(),
395 patch=options.patchset)
396 params = {'tag': 'buildset:%s' % buildset}
397
398 builds = {}
399 while True:
400 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
401 hostname=options.buildbucket_host,
402 params=urllib.urlencode(params))
403 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
404 for build in content.get('builds', []):
405 builds[build['id']] = build
406 if 'next_cursor' in content:
407 params['start_cursor'] = content['next_cursor']
408 else:
409 break
410 return builds
411
412
413def print_tryjobs(options, builds):
414 """Prints nicely result of fetch_try_jobs."""
415 if not builds:
vapiera7fbd5a2016-06-16 09:17:49 -0700416 print('No tryjobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000417 return
418
419 # Make a copy, because we'll be modifying builds dictionary.
420 builds = builds.copy()
421 builder_names_cache = {}
422
423 def get_builder(b):
424 try:
425 return builder_names_cache[b['id']]
426 except KeyError:
427 try:
428 parameters = json.loads(b['parameters_json'])
429 name = parameters['builder_name']
430 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700431 print('WARNING: failed to get builder name for build %s: %s' % (
432 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000433 name = None
434 builder_names_cache[b['id']] = name
435 return name
436
437 def get_bucket(b):
438 bucket = b['bucket']
439 if bucket.startswith('master.'):
440 return bucket[len('master.'):]
441 return bucket
442
443 if options.print_master:
444 name_fmt = '%%-%ds %%-%ds' % (
445 max(len(str(get_bucket(b))) for b in builds.itervalues()),
446 max(len(str(get_builder(b))) for b in builds.itervalues()))
447 def get_name(b):
448 return name_fmt % (get_bucket(b), get_builder(b))
449 else:
450 name_fmt = '%%-%ds' % (
451 max(len(str(get_builder(b))) for b in builds.itervalues()))
452 def get_name(b):
453 return name_fmt % get_builder(b)
454
455 def sort_key(b):
456 return b['status'], b.get('result'), get_name(b), b.get('url')
457
458 def pop(title, f, color=None, **kwargs):
459 """Pop matching builds from `builds` dict and print them."""
460
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000461 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000462 colorize = str
463 else:
464 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
465
466 result = []
467 for b in builds.values():
468 if all(b.get(k) == v for k, v in kwargs.iteritems()):
469 builds.pop(b['id'])
470 result.append(b)
471 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700472 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000473 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700474 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000475
476 total = len(builds)
477 pop(status='COMPLETED', result='SUCCESS',
478 title='Successes:', color=Fore.GREEN,
479 f=lambda b: (get_name(b), b.get('url')))
480 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
481 title='Infra Failures:', color=Fore.MAGENTA,
482 f=lambda b: (get_name(b), b.get('url')))
483 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
484 title='Failures:', color=Fore.RED,
485 f=lambda b: (get_name(b), b.get('url')))
486 pop(status='COMPLETED', result='CANCELED',
487 title='Canceled:', color=Fore.MAGENTA,
488 f=lambda b: (get_name(b),))
489 pop(status='COMPLETED', result='FAILURE',
490 failure_reason='INVALID_BUILD_DEFINITION',
491 title='Wrong master/builder name:', color=Fore.MAGENTA,
492 f=lambda b: (get_name(b),))
493 pop(status='COMPLETED', result='FAILURE',
494 title='Other failures:',
495 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
496 pop(status='COMPLETED',
497 title='Other finished:',
498 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
499 pop(status='STARTED',
500 title='Started:', color=Fore.YELLOW,
501 f=lambda b: (get_name(b), b.get('url')))
502 pop(status='SCHEDULED',
503 title='Scheduled:',
504 f=lambda b: (get_name(b), 'id=%s' % b['id']))
505 # The last section is just in case buildbucket API changes OR there is a bug.
506 pop(title='Other:',
507 f=lambda b: (get_name(b), 'id=%s' % b['id']))
508 assert len(builds) == 0
vapiera7fbd5a2016-06-16 09:17:49 -0700509 print('Total: %d tryjobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000510
511
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000512def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
513 """Return the corresponding git ref if |base_url| together with |glob_spec|
514 matches the full |url|.
515
516 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
517 """
518 fetch_suburl, as_ref = glob_spec.split(':')
519 if allow_wildcards:
520 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
521 if glob_match:
522 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
523 # "branches/{472,597,648}/src:refs/remotes/svn/*".
524 branch_re = re.escape(base_url)
525 if glob_match.group(1):
526 branch_re += '/' + re.escape(glob_match.group(1))
527 wildcard = glob_match.group(2)
528 if wildcard == '*':
529 branch_re += '([^/]*)'
530 else:
531 # Escape and replace surrounding braces with parentheses and commas
532 # with pipe symbols.
533 wildcard = re.escape(wildcard)
534 wildcard = re.sub('^\\\\{', '(', wildcard)
535 wildcard = re.sub('\\\\,', '|', wildcard)
536 wildcard = re.sub('\\\\}$', ')', wildcard)
537 branch_re += wildcard
538 if glob_match.group(3):
539 branch_re += re.escape(glob_match.group(3))
540 match = re.match(branch_re, url)
541 if match:
542 return re.sub('\*$', match.group(1), as_ref)
543
544 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
545 if fetch_suburl:
546 full_url = base_url + '/' + fetch_suburl
547 else:
548 full_url = base_url
549 if full_url == url:
550 return as_ref
551 return None
552
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000553
iannucci@chromium.org79540052012-10-19 23:15:26 +0000554def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000555 """Prints statistics about the change to the user."""
556 # --no-ext-diff is broken in some versions of Git, so try to work around
557 # this by overriding the environment (but there is still a problem if the
558 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000559 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000560 if 'GIT_EXTERNAL_DIFF' in env:
561 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000562
563 if find_copies:
564 similarity_options = ['--find-copies-harder', '-l100000',
565 '-C%s' % similarity]
566 else:
567 similarity_options = ['-M%s' % similarity]
568
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000569 try:
570 stdout = sys.stdout.fileno()
571 except AttributeError:
572 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000573 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000574 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000575 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000576 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000577
578
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000579class BuildbucketResponseException(Exception):
580 pass
581
582
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000583class Settings(object):
584 def __init__(self):
585 self.default_server = None
586 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000587 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000588 self.is_git_svn = None
589 self.svn_branch = None
590 self.tree_status_url = None
591 self.viewvc_url = None
592 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000593 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000594 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000595 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000596 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000597 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000598 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000599 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000600
601 def LazyUpdateIfNeeded(self):
602 """Updates the settings from a codereview.settings file, if available."""
603 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000604 # The only value that actually changes the behavior is
605 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000606 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000607 error_ok=True
608 ).strip().lower()
609
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000610 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000611 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000612 LoadCodereviewSettingsFromFile(cr_settings_file)
613 self.updated = True
614
615 def GetDefaultServerUrl(self, error_ok=False):
616 if not self.default_server:
617 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000618 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000619 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000620 if error_ok:
621 return self.default_server
622 if not self.default_server:
623 error_message = ('Could not find settings file. You must configure '
624 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000625 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000626 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000627 return self.default_server
628
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000629 @staticmethod
630 def GetRelativeRoot():
631 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000632
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000633 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000634 if self.root is None:
635 self.root = os.path.abspath(self.GetRelativeRoot())
636 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000637
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000638 def GetGitMirror(self, remote='origin'):
639 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000640 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000641 if not os.path.isdir(local_url):
642 return None
643 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
644 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
645 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
646 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
647 if mirror.exists():
648 return mirror
649 return None
650
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000651 def GetIsGitSvn(self):
652 """Return true if this repo looks like it's using git-svn."""
653 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000654 if self.GetPendingRefPrefix():
655 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
656 self.is_git_svn = False
657 else:
658 # If you have any "svn-remote.*" config keys, we think you're using svn.
659 self.is_git_svn = RunGitWithCode(
660 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000661 return self.is_git_svn
662
663 def GetSVNBranch(self):
664 if self.svn_branch is None:
665 if not self.GetIsGitSvn():
666 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
667
668 # Try to figure out which remote branch we're based on.
669 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000670 # 1) iterate through our branch history and find the svn URL.
671 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000672
673 # regexp matching the git-svn line that contains the URL.
674 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
675
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000676 # We don't want to go through all of history, so read a line from the
677 # pipe at a time.
678 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000679 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000680 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
681 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000682 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000683 for line in proc.stdout:
684 match = git_svn_re.match(line)
685 if match:
686 url = match.group(1)
687 proc.stdout.close() # Cut pipe.
688 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000689
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000690 if url:
691 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
692 remotes = RunGit(['config', '--get-regexp',
693 r'^svn-remote\..*\.url']).splitlines()
694 for remote in remotes:
695 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000696 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000697 remote = match.group(1)
698 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000699 rewrite_root = RunGit(
700 ['config', 'svn-remote.%s.rewriteRoot' % remote],
701 error_ok=True).strip()
702 if rewrite_root:
703 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000704 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000705 ['config', 'svn-remote.%s.fetch' % remote],
706 error_ok=True).strip()
707 if fetch_spec:
708 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
709 if self.svn_branch:
710 break
711 branch_spec = RunGit(
712 ['config', 'svn-remote.%s.branches' % remote],
713 error_ok=True).strip()
714 if branch_spec:
715 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
716 if self.svn_branch:
717 break
718 tag_spec = RunGit(
719 ['config', 'svn-remote.%s.tags' % remote],
720 error_ok=True).strip()
721 if tag_spec:
722 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
723 if self.svn_branch:
724 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000725
726 if not self.svn_branch:
727 DieWithError('Can\'t guess svn branch -- try specifying it on the '
728 'command line')
729
730 return self.svn_branch
731
732 def GetTreeStatusUrl(self, error_ok=False):
733 if not self.tree_status_url:
734 error_message = ('You must configure your tree status URL by running '
735 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000736 self.tree_status_url = self._GetRietveldConfig(
737 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000738 return self.tree_status_url
739
740 def GetViewVCUrl(self):
741 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000742 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000743 return self.viewvc_url
744
rmistry@google.com90752582014-01-14 21:04:50 +0000745 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000746 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000747
rmistry@google.com78948ed2015-07-08 23:09:57 +0000748 def GetIsSkipDependencyUpload(self, branch_name):
749 """Returns true if specified branch should skip dep uploads."""
750 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
751 error_ok=True)
752
rmistry@google.com5626a922015-02-26 14:03:30 +0000753 def GetRunPostUploadHook(self):
754 run_post_upload_hook = self._GetRietveldConfig(
755 'run-post-upload-hook', error_ok=True)
756 return run_post_upload_hook == "True"
757
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000758 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000759 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000760
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000761 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000762 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000763
ukai@chromium.orge8077812012-02-03 03:41:46 +0000764 def GetIsGerrit(self):
765 """Return true if this repo is assosiated with gerrit code review system."""
766 if self.is_gerrit is None:
767 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
768 return self.is_gerrit
769
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000770 def GetSquashGerritUploads(self):
771 """Return true if uploads to Gerrit should be squashed by default."""
772 if self.squash_gerrit_uploads is None:
773 self.squash_gerrit_uploads = (
774 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
775 error_ok=True).strip() == 'true')
776 return self.squash_gerrit_uploads
777
tandrii@chromium.org28253532016-04-14 13:46:56 +0000778 def GetGerritSkipEnsureAuthenticated(self):
779 """Return True if EnsureAuthenticated should not be done for Gerrit
780 uploads."""
781 if self.gerrit_skip_ensure_authenticated is None:
782 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000783 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000784 error_ok=True).strip() == 'true')
785 return self.gerrit_skip_ensure_authenticated
786
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000787 def GetGitEditor(self):
788 """Return the editor specified in the git config, or None if none is."""
789 if self.git_editor is None:
790 self.git_editor = self._GetConfig('core.editor', error_ok=True)
791 return self.git_editor or None
792
thestig@chromium.org44202a22014-03-11 19:22:18 +0000793 def GetLintRegex(self):
794 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
795 DEFAULT_LINT_REGEX)
796
797 def GetLintIgnoreRegex(self):
798 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
799 DEFAULT_LINT_IGNORE_REGEX)
800
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000801 def GetProject(self):
802 if not self.project:
803 self.project = self._GetRietveldConfig('project', error_ok=True)
804 return self.project
805
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000806 def GetForceHttpsCommitUrl(self):
807 if not self.force_https_commit_url:
808 self.force_https_commit_url = self._GetRietveldConfig(
809 'force-https-commit-url', error_ok=True)
810 return self.force_https_commit_url
811
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000812 def GetPendingRefPrefix(self):
813 if not self.pending_ref_prefix:
814 self.pending_ref_prefix = self._GetRietveldConfig(
815 'pending-ref-prefix', error_ok=True)
816 return self.pending_ref_prefix
817
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000818 def _GetRietveldConfig(self, param, **kwargs):
819 return self._GetConfig('rietveld.' + param, **kwargs)
820
rmistry@google.com78948ed2015-07-08 23:09:57 +0000821 def _GetBranchConfig(self, branch_name, param, **kwargs):
822 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
823
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000824 def _GetConfig(self, param, **kwargs):
825 self.LazyUpdateIfNeeded()
826 return RunGit(['config', param], **kwargs).strip()
827
828
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000829def ShortBranchName(branch):
830 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000831 return branch.replace('refs/heads/', '', 1)
832
833
834def GetCurrentBranchRef():
835 """Returns branch ref (e.g., refs/heads/master) or None."""
836 return RunGit(['symbolic-ref', 'HEAD'],
837 stderr=subprocess2.VOID, error_ok=True).strip() or None
838
839
840def GetCurrentBranch():
841 """Returns current branch or None.
842
843 For refs/heads/* branches, returns just last part. For others, full ref.
844 """
845 branchref = GetCurrentBranchRef()
846 if branchref:
847 return ShortBranchName(branchref)
848 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000849
850
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000851class _CQState(object):
852 """Enum for states of CL with respect to Commit Queue."""
853 NONE = 'none'
854 DRY_RUN = 'dry_run'
855 COMMIT = 'commit'
856
857 ALL_STATES = [NONE, DRY_RUN, COMMIT]
858
859
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000860class _ParsedIssueNumberArgument(object):
861 def __init__(self, issue=None, patchset=None, hostname=None):
862 self.issue = issue
863 self.patchset = patchset
864 self.hostname = hostname
865
866 @property
867 def valid(self):
868 return self.issue is not None
869
870
871class _RietveldParsedIssueNumberArgument(_ParsedIssueNumberArgument):
872 def __init__(self, *args, **kwargs):
873 self.patch_url = kwargs.pop('patch_url', None)
874 super(_RietveldParsedIssueNumberArgument, self).__init__(*args, **kwargs)
875
876
877def ParseIssueNumberArgument(arg):
878 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
879 fail_result = _ParsedIssueNumberArgument()
880
881 if arg.isdigit():
882 return _ParsedIssueNumberArgument(issue=int(arg))
883 if not arg.startswith('http'):
884 return fail_result
885 url = gclient_utils.UpgradeToHttps(arg)
886 try:
887 parsed_url = urlparse.urlparse(url)
888 except ValueError:
889 return fail_result
890 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
891 tmp = cls.ParseIssueURL(parsed_url)
892 if tmp is not None:
893 return tmp
894 return fail_result
895
896
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000897class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000898 """Changelist works with one changelist in local branch.
899
900 Supports two codereview backends: Rietveld or Gerrit, selected at object
901 creation.
902
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +0000903 Notes:
904 * Not safe for concurrent multi-{thread,process} use.
905 * Caches values from current branch. Therefore, re-use after branch change
906 with care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000907 """
908
909 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
910 """Create a new ChangeList instance.
911
912 If issue is given, the codereview must be given too.
913
914 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
915 Otherwise, it's decided based on current configuration of the local branch,
916 with default being 'rietveld' for backwards compatibility.
917 See _load_codereview_impl for more details.
918
919 **kwargs will be passed directly to codereview implementation.
920 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000921 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000922 global settings
923 if not settings:
924 # Happens when git_cl.py is used as a utility library.
925 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000926
927 if issue:
928 assert codereview, 'codereview must be known, if issue is known'
929
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000930 self.branchref = branchref
931 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000932 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000933 self.branch = ShortBranchName(self.branchref)
934 else:
935 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000936 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000937 self.lookedup_issue = False
938 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000939 self.has_description = False
940 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000941 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000942 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000943 self.cc = None
944 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000945 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000946
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000947 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000948 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000949 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000950 assert self._codereview_impl
951 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000952
953 def _load_codereview_impl(self, codereview=None, **kwargs):
954 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000955 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
956 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
957 self._codereview = codereview
958 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000959 return
960
961 # Automatic selection based on issue number set for a current branch.
962 # Rietveld takes precedence over Gerrit.
963 assert not self.issue
964 # Whether we find issue or not, we are doing the lookup.
965 self.lookedup_issue = True
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000966 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000967 setting = cls.IssueSetting(self.GetBranch())
968 issue = RunGit(['config', setting], error_ok=True).strip()
969 if issue:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000970 self._codereview = codereview
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000971 self._codereview_impl = cls(self, **kwargs)
972 self.issue = int(issue)
973 return
974
975 # No issue is set for this branch, so decide based on repo-wide settings.
976 return self._load_codereview_impl(
977 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
978 **kwargs)
979
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000980 def IsGerrit(self):
981 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000982
983 def GetCCList(self):
984 """Return the users cc'd on this CL.
985
986 Return is a string suitable for passing to gcl with the --cc flag.
987 """
988 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000989 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000990 more_cc = ','.join(self.watchers)
991 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
992 return self.cc
993
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000994 def GetCCListWithoutDefault(self):
995 """Return the users cc'd on this CL excluding default ones."""
996 if self.cc is None:
997 self.cc = ','.join(self.watchers)
998 return self.cc
999
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001000 def SetWatchers(self, watchers):
1001 """Set the list of email addresses that should be cc'd based on the changed
1002 files in this CL.
1003 """
1004 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001005
1006 def GetBranch(self):
1007 """Returns the short branch name, e.g. 'master'."""
1008 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001009 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001010 if not branchref:
1011 return None
1012 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001013 self.branch = ShortBranchName(self.branchref)
1014 return self.branch
1015
1016 def GetBranchRef(self):
1017 """Returns the full branch name, e.g. 'refs/heads/master'."""
1018 self.GetBranch() # Poke the lazy loader.
1019 return self.branchref
1020
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001021 def ClearBranch(self):
1022 """Clears cached branch data of this object."""
1023 self.branch = self.branchref = None
1024
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001025 @staticmethod
1026 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001027 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001028 e.g. 'origin', 'refs/heads/master'
1029 """
1030 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001031 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
1032 error_ok=True).strip()
1033 if upstream_branch:
1034 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
1035 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001036 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1037 error_ok=True).strip()
1038 if upstream_branch:
1039 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001040 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001041 # Fall back on trying a git-svn upstream branch.
1042 if settings.GetIsGitSvn():
1043 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001044 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001045 # Else, try to guess the origin remote.
1046 remote_branches = RunGit(['branch', '-r']).split()
1047 if 'origin/master' in remote_branches:
1048 # Fall back on origin/master if it exits.
1049 remote = 'origin'
1050 upstream_branch = 'refs/heads/master'
1051 elif 'origin/trunk' in remote_branches:
1052 # Fall back on origin/trunk if it exists. Generally a shared
1053 # git-svn clone
1054 remote = 'origin'
1055 upstream_branch = 'refs/heads/trunk'
1056 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001057 DieWithError(
1058 'Unable to determine default branch to diff against.\n'
1059 'Either pass complete "git diff"-style arguments, like\n'
1060 ' git cl upload origin/master\n'
1061 'or verify this branch is set up to track another \n'
1062 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001063
1064 return remote, upstream_branch
1065
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001066 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001067 upstream_branch = self.GetUpstreamBranch()
1068 if not BranchExists(upstream_branch):
1069 DieWithError('The upstream for the current branch (%s) does not exist '
1070 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001071 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001072 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001073
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001074 def GetUpstreamBranch(self):
1075 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001076 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001077 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001078 upstream_branch = upstream_branch.replace('refs/heads/',
1079 'refs/remotes/%s/' % remote)
1080 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1081 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001082 self.upstream_branch = upstream_branch
1083 return self.upstream_branch
1084
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001085 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001086 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001087 remote, branch = None, self.GetBranch()
1088 seen_branches = set()
1089 while branch not in seen_branches:
1090 seen_branches.add(branch)
1091 remote, branch = self.FetchUpstreamTuple(branch)
1092 branch = ShortBranchName(branch)
1093 if remote != '.' or branch.startswith('refs/remotes'):
1094 break
1095 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001096 remotes = RunGit(['remote'], error_ok=True).split()
1097 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001098 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001099 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001100 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001101 logging.warning('Could not determine which remote this change is '
1102 'associated with, so defaulting to "%s". This may '
1103 'not be what you want. You may prevent this message '
1104 'by running "git svn info" as documented here: %s',
1105 self._remote,
1106 GIT_INSTRUCTIONS_URL)
1107 else:
1108 logging.warn('Could not determine which remote this change is '
1109 'associated with. You may prevent this message by '
1110 'running "git svn info" as documented here: %s',
1111 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001112 branch = 'HEAD'
1113 if branch.startswith('refs/remotes'):
1114 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001115 elif branch.startswith('refs/branch-heads/'):
1116 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001117 else:
1118 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001119 return self._remote
1120
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001121 def GitSanityChecks(self, upstream_git_obj):
1122 """Checks git repo status and ensures diff is from local commits."""
1123
sbc@chromium.org79706062015-01-14 21:18:12 +00001124 if upstream_git_obj is None:
1125 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001126 print('ERROR: unable to determine current branch (detached HEAD?)',
1127 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001128 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001129 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001130 return False
1131
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001132 # Verify the commit we're diffing against is in our current branch.
1133 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1134 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1135 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001136 print('ERROR: %s is not in the current branch. You may need to rebase '
1137 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001138 return False
1139
1140 # List the commits inside the diff, and verify they are all local.
1141 commits_in_diff = RunGit(
1142 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1143 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1144 remote_branch = remote_branch.strip()
1145 if code != 0:
1146 _, remote_branch = self.GetRemoteBranch()
1147
1148 commits_in_remote = RunGit(
1149 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1150
1151 common_commits = set(commits_in_diff) & set(commits_in_remote)
1152 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001153 print('ERROR: Your diff contains %d commits already in %s.\n'
1154 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1155 'the diff. If you are using a custom git flow, you can override'
1156 ' the reference used for this check with "git config '
1157 'gitcl.remotebranch <git-ref>".' % (
1158 len(common_commits), remote_branch, upstream_git_obj),
1159 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001160 return False
1161 return True
1162
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001163 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001164 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001165
1166 Returns None if it is not set.
1167 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001168 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
1169 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001170
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001171 def GetGitSvnRemoteUrl(self):
1172 """Return the configured git-svn remote URL parsed from git svn info.
1173
1174 Returns None if it is not set.
1175 """
1176 # URL is dependent on the current directory.
1177 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1178 if data:
1179 keys = dict(line.split(': ', 1) for line in data.splitlines()
1180 if ': ' in line)
1181 return keys.get('URL', None)
1182 return None
1183
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001184 def GetRemoteUrl(self):
1185 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1186
1187 Returns None if there is no remote.
1188 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001189 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001190 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1191
1192 # If URL is pointing to a local directory, it is probably a git cache.
1193 if os.path.isdir(url):
1194 url = RunGit(['config', 'remote.%s.url' % remote],
1195 error_ok=True,
1196 cwd=url).strip()
1197 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001198
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001199 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001200 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001201 if self.issue is None and not self.lookedup_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001202 issue = RunGit(['config',
1203 self._codereview_impl.IssueSetting(self.GetBranch())],
1204 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001205 self.issue = int(issue) or None if issue else None
1206 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001207 return self.issue
1208
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001209 def GetIssueURL(self):
1210 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001211 issue = self.GetIssue()
1212 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001213 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001214 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001215
1216 def GetDescription(self, pretty=False):
1217 if not self.has_description:
1218 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001219 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001220 self.has_description = True
1221 if pretty:
1222 wrapper = textwrap.TextWrapper()
1223 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1224 return wrapper.fill(self.description)
1225 return self.description
1226
1227 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001228 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001229 if self.patchset is None and not self.lookedup_patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001230 patchset = RunGit(['config', self._codereview_impl.PatchsetSetting()],
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001231 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001232 self.patchset = int(patchset) or None if patchset else None
1233 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001234 return self.patchset
1235
1236 def SetPatchset(self, patchset):
1237 """Set this branch's patchset. If patchset=0, clears the patchset."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001238 patchset_setting = self._codereview_impl.PatchsetSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001239 if patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001240 RunGit(['config', patchset_setting, str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001241 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001242 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001243 RunGit(['config', '--unset', patchset_setting],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001244 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001245 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001246
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001247 def SetIssue(self, issue=None):
1248 """Set this branch's issue. If issue isn't given, clears the issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001249 issue_setting = self._codereview_impl.IssueSetting(self.GetBranch())
1250 codereview_setting = self._codereview_impl.GetCodereviewServerSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001251 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001252 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001253 RunGit(['config', issue_setting, str(issue)])
1254 codereview_server = self._codereview_impl.GetCodereviewServer()
1255 if codereview_server:
1256 RunGit(['config', codereview_setting, codereview_server])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001257 else:
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001258 # Reset it regardless. It doesn't hurt.
1259 config_settings = [issue_setting, self._codereview_impl.PatchsetSetting()]
1260 for prop in (['last-upload-hash'] +
1261 self._codereview_impl._PostUnsetIssueProperties()):
1262 config_settings.append('branch.%s.%s' % (self.GetBranch(), prop))
1263 for setting in config_settings:
1264 RunGit(['config', '--unset', setting], error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001265 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001266 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001267
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001268 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001269 if not self.GitSanityChecks(upstream_branch):
1270 DieWithError('\nGit sanity check failure')
1271
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001272 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001273 if not root:
1274 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001275 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001276
1277 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001278 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001279 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001280 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001281 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001282 except subprocess2.CalledProcessError:
1283 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001284 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001285 'This branch probably doesn\'t exist anymore. To reset the\n'
1286 'tracking branch, please run\n'
1287 ' git branch --set-upstream %s trunk\n'
1288 'replacing trunk with origin/master or the relevant branch') %
1289 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001290
maruel@chromium.org52424302012-08-29 15:14:30 +00001291 issue = self.GetIssue()
1292 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001293 if issue:
1294 description = self.GetDescription()
1295 else:
1296 # If the change was never uploaded, use the log messages of all commits
1297 # up to the branch point, as git cl upload will prefill the description
1298 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001299 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1300 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001301
1302 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001303 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001304 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001305 name,
1306 description,
1307 absroot,
1308 files,
1309 issue,
1310 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001311 author,
1312 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001313
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001314 def UpdateDescription(self, description):
1315 self.description = description
1316 return self._codereview_impl.UpdateDescriptionRemote(description)
1317
1318 def RunHook(self, committing, may_prompt, verbose, change):
1319 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1320 try:
1321 return presubmit_support.DoPresubmitChecks(change, committing,
1322 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1323 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001324 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1325 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001326 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001327 DieWithError(
1328 ('%s\nMaybe your depot_tools is out of date?\n'
1329 'If all fails, contact maruel@') % e)
1330
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001331 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1332 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001333 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1334 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001335 else:
1336 # Assume url.
1337 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1338 urlparse.urlparse(issue_arg))
1339 if not parsed_issue_arg or not parsed_issue_arg.valid:
1340 DieWithError('Failed to parse issue argument "%s". '
1341 'Must be an issue number or a valid URL.' % issue_arg)
1342 return self._codereview_impl.CMDPatchWithParsedIssue(
1343 parsed_issue_arg, reject, nocommit, directory)
1344
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001345 def CMDUpload(self, options, git_diff_args, orig_args):
1346 """Uploads a change to codereview."""
1347 if git_diff_args:
1348 # TODO(ukai): is it ok for gerrit case?
1349 base_branch = git_diff_args[0]
1350 else:
1351 if self.GetBranch() is None:
1352 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1353
1354 # Default to diffing against common ancestor of upstream branch
1355 base_branch = self.GetCommonAncestorWithUpstream()
1356 git_diff_args = [base_branch, 'HEAD']
1357
1358 # Make sure authenticated to codereview before running potentially expensive
1359 # hooks. It is a fast, best efforts check. Codereview still can reject the
1360 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001361 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001362
1363 # Apply watchlists on upload.
1364 change = self.GetChange(base_branch, None)
1365 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1366 files = [f.LocalPath() for f in change.AffectedFiles()]
1367 if not options.bypass_watchlists:
1368 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1369
1370 if not options.bypass_hooks:
1371 if options.reviewers or options.tbr_owners:
1372 # Set the reviewer list now so that presubmit checks can access it.
1373 change_description = ChangeDescription(change.FullDescriptionText())
1374 change_description.update_reviewers(options.reviewers,
1375 options.tbr_owners,
1376 change)
1377 change.SetDescriptionText(change_description.description)
1378 hook_results = self.RunHook(committing=False,
1379 may_prompt=not options.force,
1380 verbose=options.verbose,
1381 change=change)
1382 if not hook_results.should_continue():
1383 return 1
1384 if not options.reviewers and hook_results.reviewers:
1385 options.reviewers = hook_results.reviewers.split(',')
1386
1387 if self.GetIssue():
1388 latest_patchset = self.GetMostRecentPatchset()
1389 local_patchset = self.GetPatchset()
1390 if (latest_patchset and local_patchset and
1391 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001392 print('The last upload made from this repository was patchset #%d but '
1393 'the most recent patchset on the server is #%d.'
1394 % (local_patchset, latest_patchset))
1395 print('Uploading will still work, but if you\'ve uploaded to this '
1396 'issue from another machine or branch the patch you\'re '
1397 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001398 ask_for_data('About to upload; enter to confirm.')
1399
1400 print_stats(options.similarity, options.find_copies, git_diff_args)
1401 ret = self.CMDUploadChange(options, git_diff_args, change)
1402 if not ret:
1403 git_set_branch_value('last-upload-hash',
1404 RunGit(['rev-parse', 'HEAD']).strip())
1405 # Run post upload hooks, if specified.
1406 if settings.GetRunPostUploadHook():
1407 presubmit_support.DoPostUploadExecuter(
1408 change,
1409 self,
1410 settings.GetRoot(),
1411 options.verbose,
1412 sys.stdout)
1413
1414 # Upload all dependencies if specified.
1415 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001416 print()
1417 print('--dependencies has been specified.')
1418 print('All dependent local branches will be re-uploaded.')
1419 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001420 # Remove the dependencies flag from args so that we do not end up in a
1421 # loop.
1422 orig_args.remove('--dependencies')
1423 ret = upload_branch_deps(self, orig_args)
1424 return ret
1425
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001426 def SetCQState(self, new_state):
1427 """Update the CQ state for latest patchset.
1428
1429 Issue must have been already uploaded and known.
1430 """
1431 assert new_state in _CQState.ALL_STATES
1432 assert self.GetIssue()
1433 return self._codereview_impl.SetCQState(new_state)
1434
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001435 # Forward methods to codereview specific implementation.
1436
1437 def CloseIssue(self):
1438 return self._codereview_impl.CloseIssue()
1439
1440 def GetStatus(self):
1441 return self._codereview_impl.GetStatus()
1442
1443 def GetCodereviewServer(self):
1444 return self._codereview_impl.GetCodereviewServer()
1445
1446 def GetApprovingReviewers(self):
1447 return self._codereview_impl.GetApprovingReviewers()
1448
1449 def GetMostRecentPatchset(self):
1450 return self._codereview_impl.GetMostRecentPatchset()
1451
1452 def __getattr__(self, attr):
1453 # This is because lots of untested code accesses Rietveld-specific stuff
1454 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001455 # on a case by case basis.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001456 return getattr(self._codereview_impl, attr)
1457
1458
1459class _ChangelistCodereviewBase(object):
1460 """Abstract base class encapsulating codereview specifics of a changelist."""
1461 def __init__(self, changelist):
1462 self._changelist = changelist # instance of Changelist
1463
1464 def __getattr__(self, attr):
1465 # Forward methods to changelist.
1466 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1467 # _RietveldChangelistImpl to avoid this hack?
1468 return getattr(self._changelist, attr)
1469
1470 def GetStatus(self):
1471 """Apply a rough heuristic to give a simple summary of an issue's review
1472 or CQ status, assuming adherence to a common workflow.
1473
1474 Returns None if no issue for this branch, or specific string keywords.
1475 """
1476 raise NotImplementedError()
1477
1478 def GetCodereviewServer(self):
1479 """Returns server URL without end slash, like "https://codereview.com"."""
1480 raise NotImplementedError()
1481
1482 def FetchDescription(self):
1483 """Fetches and returns description from the codereview server."""
1484 raise NotImplementedError()
1485
1486 def GetCodereviewServerSetting(self):
1487 """Returns git config setting for the codereview server."""
1488 raise NotImplementedError()
1489
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001490 @classmethod
1491 def IssueSetting(cls, branch):
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001492 return 'branch.%s.%s' % (branch, cls.IssueSettingSuffix())
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001493
1494 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001495 def IssueSettingSuffix(cls):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001496 """Returns name of git config setting which stores issue number for a given
1497 branch."""
1498 raise NotImplementedError()
1499
1500 def PatchsetSetting(self):
1501 """Returns name of git config setting which stores issue number."""
1502 raise NotImplementedError()
1503
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001504 def _PostUnsetIssueProperties(self):
1505 """Which branch-specific properties to erase when unsettin issue."""
1506 raise NotImplementedError()
1507
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001508 def GetRieveldObjForPresubmit(self):
1509 # This is an unfortunate Rietveld-embeddedness in presubmit.
1510 # For non-Rietveld codereviews, this probably should return a dummy object.
1511 raise NotImplementedError()
1512
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001513 def GetGerritObjForPresubmit(self):
1514 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1515 return None
1516
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001517 def UpdateDescriptionRemote(self, description):
1518 """Update the description on codereview site."""
1519 raise NotImplementedError()
1520
1521 def CloseIssue(self):
1522 """Closes the issue."""
1523 raise NotImplementedError()
1524
1525 def GetApprovingReviewers(self):
1526 """Returns a list of reviewers approving the change.
1527
1528 Note: not necessarily committers.
1529 """
1530 raise NotImplementedError()
1531
1532 def GetMostRecentPatchset(self):
1533 """Returns the most recent patchset number from the codereview site."""
1534 raise NotImplementedError()
1535
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001536 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1537 directory):
1538 """Fetches and applies the issue.
1539
1540 Arguments:
1541 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1542 reject: if True, reject the failed patch instead of switching to 3-way
1543 merge. Rietveld only.
1544 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1545 only.
1546 directory: switch to directory before applying the patch. Rietveld only.
1547 """
1548 raise NotImplementedError()
1549
1550 @staticmethod
1551 def ParseIssueURL(parsed_url):
1552 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1553 failed."""
1554 raise NotImplementedError()
1555
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001556 def EnsureAuthenticated(self, force):
1557 """Best effort check that user is authenticated with codereview server.
1558
1559 Arguments:
1560 force: whether to skip confirmation questions.
1561 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001562 raise NotImplementedError()
1563
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001564 def CMDUploadChange(self, options, args, change):
1565 """Uploads a change to codereview."""
1566 raise NotImplementedError()
1567
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001568 def SetCQState(self, new_state):
1569 """Update the CQ state for latest patchset.
1570
1571 Issue must have been already uploaded and known.
1572 """
1573 raise NotImplementedError()
1574
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001575
1576class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1577 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1578 super(_RietveldChangelistImpl, self).__init__(changelist)
1579 assert settings, 'must be initialized in _ChangelistCodereviewBase'
1580 settings.GetDefaultServerUrl()
1581
1582 self._rietveld_server = rietveld_server
1583 self._auth_config = auth_config
1584 self._props = None
1585 self._rpc_server = None
1586
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001587 def GetCodereviewServer(self):
1588 if not self._rietveld_server:
1589 # If we're on a branch then get the server potentially associated
1590 # with that branch.
1591 if self.GetIssue():
1592 rietveld_server_setting = self.GetCodereviewServerSetting()
1593 if rietveld_server_setting:
1594 self._rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1595 ['config', rietveld_server_setting], error_ok=True).strip())
1596 if not self._rietveld_server:
1597 self._rietveld_server = settings.GetDefaultServerUrl()
1598 return self._rietveld_server
1599
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001600 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001601 """Best effort check that user is authenticated with Rietveld server."""
1602 if self._auth_config.use_oauth2:
1603 authenticator = auth.get_authenticator_for_host(
1604 self.GetCodereviewServer(), self._auth_config)
1605 if not authenticator.has_cached_credentials():
1606 raise auth.LoginRequiredError(self.GetCodereviewServer())
1607
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001608 def FetchDescription(self):
1609 issue = self.GetIssue()
1610 assert issue
1611 try:
1612 return self.RpcServer().get_description(issue).strip()
1613 except urllib2.HTTPError as e:
1614 if e.code == 404:
1615 DieWithError(
1616 ('\nWhile fetching the description for issue %d, received a '
1617 '404 (not found)\n'
1618 'error. It is likely that you deleted this '
1619 'issue on the server. If this is the\n'
1620 'case, please run\n\n'
1621 ' git cl issue 0\n\n'
1622 'to clear the association with the deleted issue. Then run '
1623 'this command again.') % issue)
1624 else:
1625 DieWithError(
1626 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1627 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001628 print('Warning: Failed to retrieve CL description due to network '
1629 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001630 return ''
1631
1632 def GetMostRecentPatchset(self):
1633 return self.GetIssueProperties()['patchsets'][-1]
1634
1635 def GetPatchSetDiff(self, issue, patchset):
1636 return self.RpcServer().get(
1637 '/download/issue%s_%s.diff' % (issue, patchset))
1638
1639 def GetIssueProperties(self):
1640 if self._props is None:
1641 issue = self.GetIssue()
1642 if not issue:
1643 self._props = {}
1644 else:
1645 self._props = self.RpcServer().get_issue_properties(issue, True)
1646 return self._props
1647
1648 def GetApprovingReviewers(self):
1649 return get_approving_reviewers(self.GetIssueProperties())
1650
1651 def AddComment(self, message):
1652 return self.RpcServer().add_comment(self.GetIssue(), message)
1653
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001654 def GetStatus(self):
1655 """Apply a rough heuristic to give a simple summary of an issue's review
1656 or CQ status, assuming adherence to a common workflow.
1657
1658 Returns None if no issue for this branch, or one of the following keywords:
1659 * 'error' - error from review tool (including deleted issues)
1660 * 'unsent' - not sent for review
1661 * 'waiting' - waiting for review
1662 * 'reply' - waiting for owner to reply to review
1663 * 'lgtm' - LGTM from at least one approved reviewer
1664 * 'commit' - in the commit queue
1665 * 'closed' - closed
1666 """
1667 if not self.GetIssue():
1668 return None
1669
1670 try:
1671 props = self.GetIssueProperties()
1672 except urllib2.HTTPError:
1673 return 'error'
1674
1675 if props.get('closed'):
1676 # Issue is closed.
1677 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001678 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001679 # Issue is in the commit queue.
1680 return 'commit'
1681
1682 try:
1683 reviewers = self.GetApprovingReviewers()
1684 except urllib2.HTTPError:
1685 return 'error'
1686
1687 if reviewers:
1688 # Was LGTM'ed.
1689 return 'lgtm'
1690
1691 messages = props.get('messages') or []
1692
1693 if not messages:
1694 # No message was sent.
1695 return 'unsent'
1696 if messages[-1]['sender'] != props.get('owner_email'):
1697 # Non-LGTM reply from non-owner
1698 return 'reply'
1699 return 'waiting'
1700
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001701 def UpdateDescriptionRemote(self, description):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001702 return self.RpcServer().update_description(
1703 self.GetIssue(), self.description)
1704
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001705 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001706 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001707
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001708 def SetFlag(self, flag, value):
1709 """Patchset must match."""
1710 if not self.GetPatchset():
1711 DieWithError('The patchset needs to match. Send another patchset.')
1712 try:
1713 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001714 self.GetIssue(), self.GetPatchset(), flag, value)
vapierfd77ac72016-06-16 08:33:57 -07001715 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001716 if e.code == 404:
1717 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1718 if e.code == 403:
1719 DieWithError(
1720 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1721 'match?') % (self.GetIssue(), self.GetPatchset()))
1722 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001723
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001724 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001725 """Returns an upload.RpcServer() to access this review's rietveld instance.
1726 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001727 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001728 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001729 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001730 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001731 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001732
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001733 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001734 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001735 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001736
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001737 def PatchsetSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001738 """Return the git setting that stores this change's most recent patchset."""
1739 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1740
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001741 def GetCodereviewServerSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001742 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001743 branch = self.GetBranch()
1744 if branch:
1745 return 'branch.%s.rietveldserver' % branch
1746 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001747
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001748 def _PostUnsetIssueProperties(self):
1749 """Which branch-specific properties to erase when unsetting issue."""
1750 return ['rietveldserver']
1751
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001752 def GetRieveldObjForPresubmit(self):
1753 return self.RpcServer()
1754
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001755 def SetCQState(self, new_state):
1756 props = self.GetIssueProperties()
1757 if props.get('private'):
1758 DieWithError('Cannot set-commit on private issue')
1759
1760 if new_state == _CQState.COMMIT:
1761 self.SetFlag('commit', '1')
1762 elif new_state == _CQState.NONE:
1763 self.SetFlag('commit', '0')
1764 else:
1765 raise NotImplementedError()
1766
1767
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001768 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1769 directory):
1770 # TODO(maruel): Use apply_issue.py
1771
1772 # PatchIssue should never be called with a dirty tree. It is up to the
1773 # caller to check this, but just in case we assert here since the
1774 # consequences of the caller not checking this could be dire.
1775 assert(not git_common.is_dirty_git_tree('apply'))
1776 assert(parsed_issue_arg.valid)
1777 self._changelist.issue = parsed_issue_arg.issue
1778 if parsed_issue_arg.hostname:
1779 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1780
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001781 if (isinstance(parsed_issue_arg, _RietveldParsedIssueNumberArgument) and
1782 parsed_issue_arg.patch_url):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001783 assert parsed_issue_arg.patchset
1784 patchset = parsed_issue_arg.patchset
1785 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1786 else:
1787 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1788 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1789
1790 # Switch up to the top-level directory, if necessary, in preparation for
1791 # applying the patch.
1792 top = settings.GetRelativeRoot()
1793 if top:
1794 os.chdir(top)
1795
1796 # Git patches have a/ at the beginning of source paths. We strip that out
1797 # with a sed script rather than the -p flag to patch so we can feed either
1798 # Git or svn-style patches into the same apply command.
1799 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1800 try:
1801 patch_data = subprocess2.check_output(
1802 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1803 except subprocess2.CalledProcessError:
1804 DieWithError('Git patch mungling failed.')
1805 logging.info(patch_data)
1806
1807 # We use "git apply" to apply the patch instead of "patch" so that we can
1808 # pick up file adds.
1809 # The --index flag means: also insert into the index (so we catch adds).
1810 cmd = ['git', 'apply', '--index', '-p0']
1811 if directory:
1812 cmd.extend(('--directory', directory))
1813 if reject:
1814 cmd.append('--reject')
1815 elif IsGitVersionAtLeast('1.7.12'):
1816 cmd.append('--3way')
1817 try:
1818 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
1819 stdin=patch_data, stdout=subprocess2.VOID)
1820 except subprocess2.CalledProcessError:
vapiera7fbd5a2016-06-16 09:17:49 -07001821 print('Failed to apply the patch')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001822 return 1
1823
1824 # If we had an issue, commit the current state and register the issue.
1825 if not nocommit:
1826 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
1827 'patch from issue %(i)s at patchset '
1828 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
1829 % {'i': self.GetIssue(), 'p': patchset})])
1830 self.SetIssue(self.GetIssue())
1831 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07001832 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001833 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001834 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001835 return 0
1836
1837 @staticmethod
1838 def ParseIssueURL(parsed_url):
1839 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1840 return None
1841 # Typical url: https://domain/<issue_number>[/[other]]
1842 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
1843 if match:
1844 return _RietveldParsedIssueNumberArgument(
1845 issue=int(match.group(1)),
1846 hostname=parsed_url.netloc)
1847 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
1848 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
1849 if match:
1850 return _RietveldParsedIssueNumberArgument(
1851 issue=int(match.group(1)),
1852 patchset=int(match.group(2)),
1853 hostname=parsed_url.netloc,
1854 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
1855 return None
1856
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001857 def CMDUploadChange(self, options, args, change):
1858 """Upload the patch to Rietveld."""
1859 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1860 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001861 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
1862 if options.emulate_svn_auto_props:
1863 upload_args.append('--emulate_svn_auto_props')
1864
1865 change_desc = None
1866
1867 if options.email is not None:
1868 upload_args.extend(['--email', options.email])
1869
1870 if self.GetIssue():
1871 if options.title:
1872 upload_args.extend(['--title', options.title])
1873 if options.message:
1874 upload_args.extend(['--message', options.message])
1875 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07001876 print('This branch is associated with issue %s. '
1877 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001878 else:
1879 if options.title:
1880 upload_args.extend(['--title', options.title])
1881 message = (options.title or options.message or
1882 CreateDescriptionFromLog(args))
1883 change_desc = ChangeDescription(message)
1884 if options.reviewers or options.tbr_owners:
1885 change_desc.update_reviewers(options.reviewers,
1886 options.tbr_owners,
1887 change)
1888 if not options.force:
1889 change_desc.prompt()
1890
1891 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07001892 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001893 return 1
1894
1895 upload_args.extend(['--message', change_desc.description])
1896 if change_desc.get_reviewers():
1897 upload_args.append('--reviewers=%s' % ','.join(
1898 change_desc.get_reviewers()))
1899 if options.send_mail:
1900 if not change_desc.get_reviewers():
1901 DieWithError("Must specify reviewers to send email.")
1902 upload_args.append('--send_mail')
1903
1904 # We check this before applying rietveld.private assuming that in
1905 # rietveld.cc only addresses which we can send private CLs to are listed
1906 # if rietveld.private is set, and so we should ignore rietveld.cc only
1907 # when --private is specified explicitly on the command line.
1908 if options.private:
1909 logging.warn('rietveld.cc is ignored since private flag is specified. '
1910 'You need to review and add them manually if necessary.')
1911 cc = self.GetCCListWithoutDefault()
1912 else:
1913 cc = self.GetCCList()
1914 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
1915 if cc:
1916 upload_args.extend(['--cc', cc])
1917
1918 if options.private or settings.GetDefaultPrivateFlag() == "True":
1919 upload_args.append('--private')
1920
1921 upload_args.extend(['--git_similarity', str(options.similarity)])
1922 if not options.find_copies:
1923 upload_args.extend(['--git_no_find_copies'])
1924
1925 # Include the upstream repo's URL in the change -- this is useful for
1926 # projects that have their source spread across multiple repos.
1927 remote_url = self.GetGitBaseUrlFromConfig()
1928 if not remote_url:
1929 if settings.GetIsGitSvn():
1930 remote_url = self.GetGitSvnRemoteUrl()
1931 else:
1932 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
1933 remote_url = '%s@%s' % (self.GetRemoteUrl(),
1934 self.GetUpstreamBranch().split('/')[-1])
1935 if remote_url:
1936 upload_args.extend(['--base_url', remote_url])
1937 remote, remote_branch = self.GetRemoteBranch()
1938 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
1939 settings.GetPendingRefPrefix())
1940 if target_ref:
1941 upload_args.extend(['--target_ref', target_ref])
1942
1943 # Look for dependent patchsets. See crbug.com/480453 for more details.
1944 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
1945 upstream_branch = ShortBranchName(upstream_branch)
1946 if remote is '.':
1947 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001948 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001949 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07001950 print()
1951 print('Skipping dependency patchset upload because git config '
1952 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
1953 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001954 else:
1955 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001956 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001957 auth_config=auth_config)
1958 branch_cl_issue_url = branch_cl.GetIssueURL()
1959 branch_cl_issue = branch_cl.GetIssue()
1960 branch_cl_patchset = branch_cl.GetPatchset()
1961 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
1962 upload_args.extend(
1963 ['--depends_on_patchset', '%s:%s' % (
1964 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001965 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001966 '\n'
1967 'The current branch (%s) is tracking a local branch (%s) with '
1968 'an associated CL.\n'
1969 'Adding %s/#ps%s as a dependency patchset.\n'
1970 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
1971 branch_cl_patchset))
1972
1973 project = settings.GetProject()
1974 if project:
1975 upload_args.extend(['--project', project])
1976
1977 if options.cq_dry_run:
1978 upload_args.extend(['--cq_dry_run'])
1979
1980 try:
1981 upload_args = ['upload'] + upload_args + args
1982 logging.info('upload.RealMain(%s)', upload_args)
1983 issue, patchset = upload.RealMain(upload_args)
1984 issue = int(issue)
1985 patchset = int(patchset)
1986 except KeyboardInterrupt:
1987 sys.exit(1)
1988 except:
1989 # If we got an exception after the user typed a description for their
1990 # change, back up the description before re-raising.
1991 if change_desc:
1992 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1993 print('\nGot exception while uploading -- saving description to %s\n' %
1994 backup_path)
1995 backup_file = open(backup_path, 'w')
1996 backup_file.write(change_desc.description)
1997 backup_file.close()
1998 raise
1999
2000 if not self.GetIssue():
2001 self.SetIssue(issue)
2002 self.SetPatchset(patchset)
2003
2004 if options.use_commit_queue:
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002005 self.SetCQState(_CQState.COMMIT)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002006 return 0
2007
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002008
2009class _GerritChangelistImpl(_ChangelistCodereviewBase):
2010 def __init__(self, changelist, auth_config=None):
2011 # auth_config is Rietveld thing, kept here to preserve interface only.
2012 super(_GerritChangelistImpl, self).__init__(changelist)
2013 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002014 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002015 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002016 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002017
2018 def _GetGerritHost(self):
2019 # Lazy load of configs.
2020 self.GetCodereviewServer()
2021 return self._gerrit_host
2022
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002023 def _GetGitHost(self):
2024 """Returns git host to be used when uploading change to Gerrit."""
2025 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2026
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002027 def GetCodereviewServer(self):
2028 if not self._gerrit_server:
2029 # If we're on a branch then get the server potentially associated
2030 # with that branch.
2031 if self.GetIssue():
2032 gerrit_server_setting = self.GetCodereviewServerSetting()
2033 if gerrit_server_setting:
2034 self._gerrit_server = RunGit(['config', gerrit_server_setting],
2035 error_ok=True).strip()
2036 if self._gerrit_server:
2037 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
2038 if not self._gerrit_server:
2039 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2040 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002041 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002042 parts[0] = parts[0] + '-review'
2043 self._gerrit_host = '.'.join(parts)
2044 self._gerrit_server = 'https://%s' % self._gerrit_host
2045 return self._gerrit_server
2046
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002047 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00002048 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002049 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002050
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002051 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002052 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002053 if settings.GetGerritSkipEnsureAuthenticated():
2054 # For projects with unusual authentication schemes.
2055 # See http://crbug.com/603378.
2056 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002057 # Lazy-loader to identify Gerrit and Git hosts.
2058 if gerrit_util.GceAuthenticator.is_gce():
2059 return
2060 self.GetCodereviewServer()
2061 git_host = self._GetGitHost()
2062 assert self._gerrit_server and self._gerrit_host
2063 cookie_auth = gerrit_util.CookiesAuthenticator()
2064
2065 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2066 git_auth = cookie_auth.get_auth_header(git_host)
2067 if gerrit_auth and git_auth:
2068 if gerrit_auth == git_auth:
2069 return
2070 print((
2071 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2072 ' Check your %s or %s file for credentials of hosts:\n'
2073 ' %s\n'
2074 ' %s\n'
2075 ' %s') %
2076 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2077 git_host, self._gerrit_host,
2078 cookie_auth.get_new_password_message(git_host)))
2079 if not force:
2080 ask_for_data('If you know what you are doing, press Enter to continue, '
2081 'Ctrl+C to abort.')
2082 return
2083 else:
2084 missing = (
2085 [] if gerrit_auth else [self._gerrit_host] +
2086 [] if git_auth else [git_host])
2087 DieWithError('Credentials for the following hosts are required:\n'
2088 ' %s\n'
2089 'These are read from %s (or legacy %s)\n'
2090 '%s' % (
2091 '\n '.join(missing),
2092 cookie_auth.get_gitcookies_path(),
2093 cookie_auth.get_netrc_path(),
2094 cookie_auth.get_new_password_message(git_host)))
2095
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002096
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002097 def PatchsetSetting(self):
2098 """Return the git setting that stores this change's most recent patchset."""
2099 return 'branch.%s.gerritpatchset' % self.GetBranch()
2100
2101 def GetCodereviewServerSetting(self):
2102 """Returns the git setting that stores this change's Gerrit server."""
2103 branch = self.GetBranch()
2104 if branch:
2105 return 'branch.%s.gerritserver' % branch
2106 return None
2107
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002108 def _PostUnsetIssueProperties(self):
2109 """Which branch-specific properties to erase when unsetting issue."""
2110 return [
2111 'gerritserver',
2112 'gerritsquashhash',
2113 ]
2114
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002115 def GetRieveldObjForPresubmit(self):
2116 class ThisIsNotRietveldIssue(object):
2117 def __nonzero__(self):
2118 # This is a hack to make presubmit_support think that rietveld is not
2119 # defined, yet still ensure that calls directly result in a decent
2120 # exception message below.
2121 return False
2122
2123 def __getattr__(self, attr):
2124 print(
2125 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2126 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2127 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2128 'or use Rietveld for codereview.\n'
2129 'See also http://crbug.com/579160.' % attr)
2130 raise NotImplementedError()
2131 return ThisIsNotRietveldIssue()
2132
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002133 def GetGerritObjForPresubmit(self):
2134 return presubmit_support.GerritAccessor(self._GetGerritHost())
2135
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002136 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002137 """Apply a rough heuristic to give a simple summary of an issue's review
2138 or CQ status, assuming adherence to a common workflow.
2139
2140 Returns None if no issue for this branch, or one of the following keywords:
2141 * 'error' - error from review tool (including deleted issues)
2142 * 'unsent' - no reviewers added
2143 * 'waiting' - waiting for review
2144 * 'reply' - waiting for owner to reply to review
2145 * 'not lgtm' - Code-Review -2 from at least one approved reviewer
2146 * 'lgtm' - Code-Review +2 from at least one approved reviewer
2147 * 'commit' - in the commit queue
2148 * 'closed' - abandoned
2149 """
2150 if not self.GetIssue():
2151 return None
2152
2153 try:
2154 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
2155 except httplib.HTTPException:
2156 return 'error'
2157
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002158 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002159 return 'closed'
2160
2161 cq_label = data['labels'].get('Commit-Queue', {})
2162 if cq_label:
2163 # Vote value is a stringified integer, which we expect from 0 to 2.
2164 vote_value = cq_label.get('value', '0')
2165 vote_text = cq_label.get('values', {}).get(vote_value, '')
2166 if vote_text.lower() == 'commit':
2167 return 'commit'
2168
2169 lgtm_label = data['labels'].get('Code-Review', {})
2170 if lgtm_label:
2171 if 'rejected' in lgtm_label:
2172 return 'not lgtm'
2173 if 'approved' in lgtm_label:
2174 return 'lgtm'
2175
2176 if not data.get('reviewers', {}).get('REVIEWER', []):
2177 return 'unsent'
2178
2179 messages = data.get('messages', [])
2180 if messages:
2181 owner = data['owner'].get('_account_id')
2182 last_message_author = messages[-1].get('author', {}).get('_account_id')
2183 if owner != last_message_author:
2184 # Some reply from non-owner.
2185 return 'reply'
2186
2187 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002188
2189 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002190 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002191 return data['revisions'][data['current_revision']]['_number']
2192
2193 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002194 data = self._GetChangeDetail(['CURRENT_REVISION'])
2195 current_rev = data['current_revision']
2196 url = data['revisions'][current_rev]['fetch']['http']['url']
2197 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002198
2199 def UpdateDescriptionRemote(self, description):
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002200 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
2201 description)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002202
2203 def CloseIssue(self):
2204 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2205
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002206 def GetApprovingReviewers(self):
2207 """Returns a list of reviewers approving the change.
2208
2209 Note: not necessarily committers.
2210 """
2211 raise NotImplementedError()
2212
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002213 def SubmitIssue(self, wait_for_merge=True):
2214 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2215 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002216
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002217 def _GetChangeDetail(self, options=None, issue=None):
2218 options = options or []
2219 issue = issue or self.GetIssue()
2220 assert issue, 'issue required to query Gerrit'
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002221 return gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2222 options)
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002223
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002224 def CMDLand(self, force, bypass_hooks, verbose):
2225 if git_common.is_dirty_git_tree('land'):
2226 return 1
2227 differs = True
2228 last_upload = RunGit(['config',
2229 'branch.%s.gerritsquashhash' % self.GetBranch()],
2230 error_ok=True).strip()
2231 # Note: git diff outputs nothing if there is no diff.
2232 if not last_upload or RunGit(['diff', last_upload]).strip():
2233 print('WARNING: some changes from local branch haven\'t been uploaded')
2234 else:
2235 detail = self._GetChangeDetail(['CURRENT_REVISION'])
2236 if detail['current_revision'] == last_upload:
2237 differs = False
2238 else:
2239 print('WARNING: local branch contents differ from latest uploaded '
2240 'patchset')
2241 if differs:
2242 if not force:
2243 ask_for_data(
2244 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2245 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2246 elif not bypass_hooks:
2247 hook_results = self.RunHook(
2248 committing=True,
2249 may_prompt=not force,
2250 verbose=verbose,
2251 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2252 if not hook_results.should_continue():
2253 return 1
2254
2255 self.SubmitIssue(wait_for_merge=True)
2256 print('Issue %s has been submitted.' % self.GetIssueURL())
2257 return 0
2258
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002259 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2260 directory):
2261 assert not reject
2262 assert not nocommit
2263 assert not directory
2264 assert parsed_issue_arg.valid
2265
2266 self._changelist.issue = parsed_issue_arg.issue
2267
2268 if parsed_issue_arg.hostname:
2269 self._gerrit_host = parsed_issue_arg.hostname
2270 self._gerrit_server = 'https://%s' % self._gerrit_host
2271
2272 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2273
2274 if not parsed_issue_arg.patchset:
2275 # Use current revision by default.
2276 revision_info = detail['revisions'][detail['current_revision']]
2277 patchset = int(revision_info['_number'])
2278 else:
2279 patchset = parsed_issue_arg.patchset
2280 for revision_info in detail['revisions'].itervalues():
2281 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2282 break
2283 else:
2284 DieWithError('Couldn\'t find patchset %i in issue %i' %
2285 (parsed_issue_arg.patchset, self.GetIssue()))
2286
2287 fetch_info = revision_info['fetch']['http']
2288 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2289 RunGit(['cherry-pick', 'FETCH_HEAD'])
2290 self.SetIssue(self.GetIssue())
2291 self.SetPatchset(patchset)
2292 print('Committed patch for issue %i pathset %i locally' %
2293 (self.GetIssue(), self.GetPatchset()))
2294 return 0
2295
2296 @staticmethod
2297 def ParseIssueURL(parsed_url):
2298 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2299 return None
2300 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2301 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2302 # Short urls like https://domain/<issue_number> can be used, but don't allow
2303 # specifying the patchset (you'd 404), but we allow that here.
2304 if parsed_url.path == '/':
2305 part = parsed_url.fragment
2306 else:
2307 part = parsed_url.path
2308 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2309 if match:
2310 return _ParsedIssueNumberArgument(
2311 issue=int(match.group(2)),
2312 patchset=int(match.group(4)) if match.group(4) else None,
2313 hostname=parsed_url.netloc)
2314 return None
2315
tandrii16e0b4e2016-06-07 10:34:28 -07002316 def _GerritCommitMsgHookCheck(self, offer_removal):
2317 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2318 if not os.path.exists(hook):
2319 return
2320 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2321 # custom developer made one.
2322 data = gclient_utils.FileRead(hook)
2323 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2324 return
2325 print('Warning: you have Gerrit commit-msg hook installed.\n'
2326 'It is not neccessary for uploading with git cl in squash mode, '
2327 'and may interfere with it in subtle ways.\n'
2328 'We recommend you remove the commit-msg hook.')
2329 if offer_removal:
2330 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2331 if reply.lower().startswith('y'):
2332 gclient_utils.rm_file_or_tree(hook)
2333 print('Gerrit commit-msg hook removed.')
2334 else:
2335 print('OK, will keep Gerrit commit-msg hook in place.')
2336
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002337 def CMDUploadChange(self, options, args, change):
2338 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002339 if options.squash and options.no_squash:
2340 DieWithError('Can only use one of --squash or --no-squash')
2341 options.squash = ((settings.GetSquashGerritUploads() or options.squash) and
2342 not options.no_squash)
tandrii26f3e4e2016-06-10 08:37:04 -07002343
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002344 # We assume the remote called "origin" is the one we want.
2345 # It is probably not worthwhile to support different workflows.
2346 gerrit_remote = 'origin'
2347
2348 remote, remote_branch = self.GetRemoteBranch()
2349 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2350 pending_prefix='')
2351
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002352 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002353 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002354 if not self.GetIssue():
2355 # TODO(tandrii): deperecate this after 2016Q2. Backwards compatibility
2356 # with shadow branch, which used to contain change-id for a given
2357 # branch, using which we can fetch actual issue number and set it as the
2358 # property of the branch, which is the new way.
2359 message = RunGitSilent([
2360 'show', '--format=%B', '-s',
2361 'refs/heads/git_cl_uploads/%s' % self.GetBranch()])
2362 if message:
2363 change_ids = git_footers.get_footer_change_id(message.strip())
2364 if change_ids and len(change_ids) == 1:
2365 details = self._GetChangeDetail(issue=change_ids[0])
2366 if details:
2367 print('WARNING: found old upload in branch git_cl_uploads/%s '
2368 'corresponding to issue %s' %
2369 (self.GetBranch(), details['_number']))
2370 self.SetIssue(details['_number'])
2371 if not self.GetIssue():
2372 DieWithError(
2373 '\n' # For readability of the blob below.
2374 'Found old upload in branch git_cl_uploads/%s, '
2375 'but failed to find corresponding Gerrit issue.\n'
2376 'If you know the issue number, set it manually first:\n'
2377 ' git cl issue 123456\n'
2378 'If you intended to upload this CL as new issue, '
2379 'just delete or rename the old upload branch:\n'
2380 ' git rename-branch git_cl_uploads/%s old_upload-%s\n'
2381 'After that, please run git cl upload again.' %
2382 tuple([self.GetBranch()] * 3))
2383 # End of backwards compatability.
2384
2385 if self.GetIssue():
2386 # Try to get the message from a previous upload.
2387 message = self.GetDescription()
2388 if not message:
2389 DieWithError(
2390 'failed to fetch description from current Gerrit issue %d\n'
2391 '%s' % (self.GetIssue(), self.GetIssueURL()))
2392 change_id = self._GetChangeDetail()['change_id']
2393 while True:
2394 footer_change_ids = git_footers.get_footer_change_id(message)
2395 if footer_change_ids == [change_id]:
2396 break
2397 if not footer_change_ids:
2398 message = git_footers.add_footer_change_id(message, change_id)
2399 print('WARNING: appended missing Change-Id to issue description')
2400 continue
2401 # There is already a valid footer but with different or several ids.
2402 # Doing this automatically is non-trivial as we don't want to lose
2403 # existing other footers, yet we want to append just 1 desired
2404 # Change-Id. Thus, just create a new footer, but let user verify the
2405 # new description.
2406 message = '%s\n\nChange-Id: %s' % (message, change_id)
2407 print(
2408 'WARNING: issue %s has Change-Id footer(s):\n'
2409 ' %s\n'
2410 'but issue has Change-Id %s, according to Gerrit.\n'
2411 'Please, check the proposed correction to the description, '
2412 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2413 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2414 change_id))
2415 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2416 if not options.force:
2417 change_desc = ChangeDescription(message)
2418 change_desc.prompt()
2419 message = change_desc.description
2420 if not message:
2421 DieWithError("Description is empty. Aborting...")
2422 # Continue the while loop.
2423 # Sanity check of this code - we should end up with proper message
2424 # footer.
2425 assert [change_id] == git_footers.get_footer_change_id(message)
2426 change_desc = ChangeDescription(message)
2427 else:
2428 change_desc = ChangeDescription(
2429 options.message or CreateDescriptionFromLog(args))
2430 if not options.force:
2431 change_desc.prompt()
2432 if not change_desc.description:
2433 DieWithError("Description is empty. Aborting...")
2434 message = change_desc.description
2435 change_ids = git_footers.get_footer_change_id(message)
2436 if len(change_ids) > 1:
2437 DieWithError('too many Change-Id footers, at most 1 allowed.')
2438 if not change_ids:
2439 # Generate the Change-Id automatically.
2440 message = git_footers.add_footer_change_id(
2441 message, GenerateGerritChangeId(message))
2442 change_desc.set_description(message)
2443 change_ids = git_footers.get_footer_change_id(message)
2444 assert len(change_ids) == 1
2445 change_id = change_ids[0]
2446
2447 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2448 if remote is '.':
2449 # If our upstream branch is local, we base our squashed commit on its
2450 # squashed version.
2451 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2452 # Check the squashed hash of the parent.
2453 parent = RunGit(['config',
2454 'branch.%s.gerritsquashhash' % upstream_branch_name],
2455 error_ok=True).strip()
2456 # Verify that the upstream branch has been uploaded too, otherwise
2457 # Gerrit will create additional CLs when uploading.
2458 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2459 RunGitSilent(['rev-parse', parent + ':'])):
2460 # TODO(tandrii): remove "old depot_tools" part on April 12, 2016.
2461 DieWithError(
2462 'Upload upstream branch %s first.\n'
2463 'Note: maybe you\'ve uploaded it with --no-squash or with an old '
2464 'version of depot_tools. If so, then re-upload it with:\n'
2465 ' git cl upload --squash\n' % upstream_branch_name)
2466 else:
2467 parent = self.GetCommonAncestorWithUpstream()
2468
2469 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2470 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2471 '-m', message]).strip()
2472 else:
2473 change_desc = ChangeDescription(
2474 options.message or CreateDescriptionFromLog(args))
2475 if not change_desc.description:
2476 DieWithError("Description is empty. Aborting...")
2477
2478 if not git_footers.get_footer_change_id(change_desc.description):
2479 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002480 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2481 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002482 ref_to_push = 'HEAD'
2483 parent = '%s/%s' % (gerrit_remote, branch)
2484 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2485
2486 assert change_desc
2487 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2488 ref_to_push)]).splitlines()
2489 if len(commits) > 1:
2490 print('WARNING: This will upload %d commits. Run the following command '
2491 'to see which commits will be uploaded: ' % len(commits))
2492 print('git log %s..%s' % (parent, ref_to_push))
2493 print('You can also use `git squash-branch` to squash these into a '
2494 'single commit.')
2495 ask_for_data('About to upload; enter to confirm.')
2496
2497 if options.reviewers or options.tbr_owners:
2498 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2499 change)
2500
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002501 # Extra options that can be specified at push time. Doc:
2502 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2503 refspec_opts = []
2504 if options.title:
2505 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2506 # reverse on its side.
2507 if '_' in options.title:
2508 print('WARNING: underscores in title will be converted to spaces.')
2509 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2510
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002511 if options.send_mail:
2512 if not change_desc.get_reviewers():
2513 DieWithError('Must specify reviewers to send email.')
2514 refspec_opts.append('notify=ALL')
2515 else:
2516 refspec_opts.append('notify=NONE')
2517
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002518 cc = self.GetCCList().split(',')
2519 if options.cc:
2520 cc.extend(options.cc)
2521 cc = filter(None, cc)
2522 if cc:
tandrii@chromium.org074c2af2016-06-03 23:18:40 +00002523 refspec_opts.extend('cc=' + email.strip() for email in cc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002524
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002525 if change_desc.get_reviewers():
2526 refspec_opts.extend('r=' + email.strip()
2527 for email in change_desc.get_reviewers())
2528
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002529 refspec_suffix = ''
2530 if refspec_opts:
2531 refspec_suffix = '%' + ','.join(refspec_opts)
2532 assert ' ' not in refspec_suffix, (
2533 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002534 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002535
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002536 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002537 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002538 print_stdout=True,
2539 # Flush after every line: useful for seeing progress when running as
2540 # recipe.
2541 filter_fn=lambda _: sys.stdout.flush())
2542
2543 if options.squash:
2544 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2545 change_numbers = [m.group(1)
2546 for m in map(regex.match, push_stdout.splitlines())
2547 if m]
2548 if len(change_numbers) != 1:
2549 DieWithError(
2550 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2551 'Change-Id: %s') % (len(change_numbers), change_id))
2552 self.SetIssue(change_numbers[0])
2553 RunGit(['config', 'branch.%s.gerritsquashhash' % self.GetBranch(),
2554 ref_to_push])
2555 return 0
2556
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002557 def _AddChangeIdToCommitMessage(self, options, args):
2558 """Re-commits using the current message, assumes the commit hook is in
2559 place.
2560 """
2561 log_desc = options.message or CreateDescriptionFromLog(args)
2562 git_command = ['commit', '--amend', '-m', log_desc]
2563 RunGit(git_command)
2564 new_log_desc = CreateDescriptionFromLog(args)
2565 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002566 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002567 return new_log_desc
2568 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002569 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002570
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002571 def SetCQState(self, new_state):
2572 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
2573 # TODO(tandrii): maybe allow configurability in codereview.settings or by
2574 # self-discovery of label config for this CL using REST API.
2575 vote_map = {
2576 _CQState.NONE: 0,
2577 _CQState.DRY_RUN: 1,
2578 _CQState.COMMIT : 2,
2579 }
2580 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2581 labels={'Commit-Queue': vote_map[new_state]})
2582
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002583
2584_CODEREVIEW_IMPLEMENTATIONS = {
2585 'rietveld': _RietveldChangelistImpl,
2586 'gerrit': _GerritChangelistImpl,
2587}
2588
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002589
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002590def _add_codereview_select_options(parser):
2591 """Appends --gerrit and --rietveld options to force specific codereview."""
2592 parser.codereview_group = optparse.OptionGroup(
2593 parser, 'EXPERIMENTAL! Codereview override options')
2594 parser.add_option_group(parser.codereview_group)
2595 parser.codereview_group.add_option(
2596 '--gerrit', action='store_true',
2597 help='Force the use of Gerrit for codereview')
2598 parser.codereview_group.add_option(
2599 '--rietveld', action='store_true',
2600 help='Force the use of Rietveld for codereview')
2601
2602
2603def _process_codereview_select_options(parser, options):
2604 if options.gerrit and options.rietveld:
2605 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2606 options.forced_codereview = None
2607 if options.gerrit:
2608 options.forced_codereview = 'gerrit'
2609 elif options.rietveld:
2610 options.forced_codereview = 'rietveld'
2611
2612
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002613class ChangeDescription(object):
2614 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002615 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002616 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002617
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002618 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002619 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002620
agable@chromium.org42c20792013-09-12 17:34:49 +00002621 @property # www.logilab.org/ticket/89786
2622 def description(self): # pylint: disable=E0202
2623 return '\n'.join(self._description_lines)
2624
2625 def set_description(self, desc):
2626 if isinstance(desc, basestring):
2627 lines = desc.splitlines()
2628 else:
2629 lines = [line.rstrip() for line in desc]
2630 while lines and not lines[0]:
2631 lines.pop(0)
2632 while lines and not lines[-1]:
2633 lines.pop(-1)
2634 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002635
piman@chromium.org336f9122014-09-04 02:16:55 +00002636 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002637 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002638 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002639 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002640 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002641 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002642
agable@chromium.org42c20792013-09-12 17:34:49 +00002643 # Get the set of R= and TBR= lines and remove them from the desciption.
2644 regexp = re.compile(self.R_LINE)
2645 matches = [regexp.match(line) for line in self._description_lines]
2646 new_desc = [l for i, l in enumerate(self._description_lines)
2647 if not matches[i]]
2648 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002649
agable@chromium.org42c20792013-09-12 17:34:49 +00002650 # Construct new unified R= and TBR= lines.
2651 r_names = []
2652 tbr_names = []
2653 for match in matches:
2654 if not match:
2655 continue
2656 people = cleanup_list([match.group(2).strip()])
2657 if match.group(1) == 'TBR':
2658 tbr_names.extend(people)
2659 else:
2660 r_names.extend(people)
2661 for name in r_names:
2662 if name not in reviewers:
2663 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002664 if add_owners_tbr:
2665 owners_db = owners.Database(change.RepositoryRoot(),
2666 fopen=file, os_path=os.path, glob=glob.glob)
2667 all_reviewers = set(tbr_names + reviewers)
2668 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2669 all_reviewers)
2670 tbr_names.extend(owners_db.reviewers_for(missing_files,
2671 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002672 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2673 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2674
2675 # Put the new lines in the description where the old first R= line was.
2676 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2677 if 0 <= line_loc < len(self._description_lines):
2678 if new_tbr_line:
2679 self._description_lines.insert(line_loc, new_tbr_line)
2680 if new_r_line:
2681 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002682 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002683 if new_r_line:
2684 self.append_footer(new_r_line)
2685 if new_tbr_line:
2686 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002687
2688 def prompt(self):
2689 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002690 self.set_description([
2691 '# Enter a description of the change.',
2692 '# This will be displayed on the codereview site.',
2693 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002694 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002695 '--------------------',
2696 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002697
agable@chromium.org42c20792013-09-12 17:34:49 +00002698 regexp = re.compile(self.BUG_LINE)
2699 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00002700 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00002701 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002702 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002703 if not content:
2704 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002705 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002706
2707 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002708 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2709 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002710 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002711 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002712
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002713 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00002714 """Adds a footer line to the description.
2715
2716 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
2717 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
2718 that Gerrit footers are always at the end.
2719 """
2720 parsed_footer_line = git_footers.parse_footer(line)
2721 if parsed_footer_line:
2722 # Line is a gerrit footer in the form: Footer-Key: any value.
2723 # Thus, must be appended observing Gerrit footer rules.
2724 self.set_description(
2725 git_footers.add_footer(self.description,
2726 key=parsed_footer_line[0],
2727 value=parsed_footer_line[1]))
2728 return
2729
2730 if not self._description_lines:
2731 self._description_lines.append(line)
2732 return
2733
2734 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
2735 if gerrit_footers:
2736 # git_footers.split_footers ensures that there is an empty line before
2737 # actual (gerrit) footers, if any. We have to keep it that way.
2738 assert top_lines and top_lines[-1] == ''
2739 top_lines, separator = top_lines[:-1], top_lines[-1:]
2740 else:
2741 separator = [] # No need for separator if there are no gerrit_footers.
2742
2743 prev_line = top_lines[-1] if top_lines else ''
2744 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
2745 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2746 top_lines.append('')
2747 top_lines.append(line)
2748 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002749
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002750 def get_reviewers(self):
2751 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002752 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
2753 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002754 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002755
2756
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002757def get_approving_reviewers(props):
2758 """Retrieves the reviewers that approved a CL from the issue properties with
2759 messages.
2760
2761 Note that the list may contain reviewers that are not committer, thus are not
2762 considered by the CQ.
2763 """
2764 return sorted(
2765 set(
2766 message['sender']
2767 for message in props['messages']
2768 if message['approval'] and message['sender'] in props['reviewers']
2769 )
2770 )
2771
2772
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002773def FindCodereviewSettingsFile(filename='codereview.settings'):
2774 """Finds the given file starting in the cwd and going up.
2775
2776 Only looks up to the top of the repository unless an
2777 'inherit-review-settings-ok' file exists in the root of the repository.
2778 """
2779 inherit_ok_file = 'inherit-review-settings-ok'
2780 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002781 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002782 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2783 root = '/'
2784 while True:
2785 if filename in os.listdir(cwd):
2786 if os.path.isfile(os.path.join(cwd, filename)):
2787 return open(os.path.join(cwd, filename))
2788 if cwd == root:
2789 break
2790 cwd = os.path.dirname(cwd)
2791
2792
2793def LoadCodereviewSettingsFromFile(fileobj):
2794 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002795 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002796
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002797 def SetProperty(name, setting, unset_error_ok=False):
2798 fullname = 'rietveld.' + name
2799 if setting in keyvals:
2800 RunGit(['config', fullname, keyvals[setting]])
2801 else:
2802 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2803
2804 SetProperty('server', 'CODE_REVIEW_SERVER')
2805 # Only server setting is required. Other settings can be absent.
2806 # In that case, we ignore errors raised during option deletion attempt.
2807 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002808 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002809 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2810 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002811 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002812 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002813 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2814 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002815 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002816 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002817 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002818 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2819 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002820
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002821 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002822 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002823
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002824 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07002825 RunGit(['config', 'gerrit.squash-uploads',
2826 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002827
tandrii@chromium.org28253532016-04-14 13:46:56 +00002828 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00002829 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00002830 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
2831
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002832 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2833 #should be of the form
2834 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2835 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2836 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2837 keyvals['ORIGIN_URL_CONFIG']])
2838
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002839
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002840def urlretrieve(source, destination):
2841 """urllib is broken for SSL connections via a proxy therefore we
2842 can't use urllib.urlretrieve()."""
2843 with open(destination, 'w') as f:
2844 f.write(urllib2.urlopen(source).read())
2845
2846
ukai@chromium.org712d6102013-11-27 00:52:58 +00002847def hasSheBang(fname):
2848 """Checks fname is a #! script."""
2849 with open(fname) as f:
2850 return f.read(2).startswith('#!')
2851
2852
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00002853# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
2854def DownloadHooks(*args, **kwargs):
2855 pass
2856
2857
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002858def DownloadGerritHook(force):
2859 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002860
2861 Args:
2862 force: True to update hooks. False to install hooks if not present.
2863 """
2864 if not settings.GetIsGerrit():
2865 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00002866 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002867 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2868 if not os.access(dst, os.X_OK):
2869 if os.path.exists(dst):
2870 if not force:
2871 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002872 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002873 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002874 if not hasSheBang(dst):
2875 DieWithError('Not a script: %s\n'
2876 'You need to download from\n%s\n'
2877 'into .git/hooks/commit-msg and '
2878 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002879 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2880 except Exception:
2881 if os.path.exists(dst):
2882 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002883 DieWithError('\nFailed to download hooks.\n'
2884 'You need to download from\n%s\n'
2885 'into .git/hooks/commit-msg and '
2886 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002887
2888
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002889
2890def GetRietveldCodereviewSettingsInteractively():
2891 """Prompt the user for settings."""
2892 server = settings.GetDefaultServerUrl(error_ok=True)
2893 prompt = 'Rietveld server (host[:port])'
2894 prompt += ' [%s]' % (server or DEFAULT_SERVER)
2895 newserver = ask_for_data(prompt + ':')
2896 if not server and not newserver:
2897 newserver = DEFAULT_SERVER
2898 if newserver:
2899 newserver = gclient_utils.UpgradeToHttps(newserver)
2900 if newserver != server:
2901 RunGit(['config', 'rietveld.server', newserver])
2902
2903 def SetProperty(initial, caption, name, is_url):
2904 prompt = caption
2905 if initial:
2906 prompt += ' ("x" to clear) [%s]' % initial
2907 new_val = ask_for_data(prompt + ':')
2908 if new_val == 'x':
2909 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
2910 elif new_val:
2911 if is_url:
2912 new_val = gclient_utils.UpgradeToHttps(new_val)
2913 if new_val != initial:
2914 RunGit(['config', 'rietveld.' + name, new_val])
2915
2916 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
2917 SetProperty(settings.GetDefaultPrivateFlag(),
2918 'Private flag (rietveld only)', 'private', False)
2919 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
2920 'tree-status-url', False)
2921 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
2922 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
2923 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
2924 'run-post-upload-hook', False)
2925
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002926@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002927def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002928 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002929
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002930 print('WARNING: git cl config works for Rietveld only.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002931 'For Gerrit, see http://crbug.com/603116.')
2932 # TODO(tandrii): add Gerrit support as part of http://crbug.com/603116.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00002933 parser.add_option('--activate-update', action='store_true',
2934 help='activate auto-updating [rietveld] section in '
2935 '.git/config')
2936 parser.add_option('--deactivate-update', action='store_true',
2937 help='deactivate auto-updating [rietveld] section in '
2938 '.git/config')
2939 options, args = parser.parse_args(args)
2940
2941 if options.deactivate_update:
2942 RunGit(['config', 'rietveld.autoupdate', 'false'])
2943 return
2944
2945 if options.activate_update:
2946 RunGit(['config', '--unset', 'rietveld.autoupdate'])
2947 return
2948
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002949 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002950 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002951 return 0
2952
2953 url = args[0]
2954 if not url.endswith('codereview.settings'):
2955 url = os.path.join(url, 'codereview.settings')
2956
2957 # Load code review settings and download hooks (if available).
2958 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
2959 return 0
2960
2961
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002962def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002963 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002964 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
2965 branch = ShortBranchName(branchref)
2966 _, args = parser.parse_args(args)
2967 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07002968 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002969 return RunGit(['config', 'branch.%s.base-url' % branch],
2970 error_ok=False).strip()
2971 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002972 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002973 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
2974 error_ok=False).strip()
2975
2976
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002977def color_for_status(status):
2978 """Maps a Changelist status to color, for CMDstatus and other tools."""
2979 return {
2980 'unsent': Fore.RED,
2981 'waiting': Fore.BLUE,
2982 'reply': Fore.YELLOW,
2983 'lgtm': Fore.GREEN,
2984 'commit': Fore.MAGENTA,
2985 'closed': Fore.CYAN,
2986 'error': Fore.WHITE,
2987 }.get(status, Fore.WHITE)
2988
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00002989
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002990def get_cl_statuses(changes, fine_grained, max_processes=None):
2991 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002992
2993 If fine_grained is true, this will fetch CL statuses from the server.
2994 Otherwise, simply indicate if there's a matching url for the given branches.
2995
2996 If max_processes is specified, it is used as the maximum number of processes
2997 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
2998 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00002999
3000 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003001 """
3002 # Silence upload.py otherwise it becomes unwieldly.
3003 upload.verbosity = 0
3004
3005 if fine_grained:
3006 # Process one branch synchronously to work through authentication, then
3007 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003008 if changes:
3009 fetch = lambda cl: (cl, cl.GetStatus())
3010 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003011
kmarshall3bff56b2016-06-06 18:31:47 -07003012 if not changes:
3013 # Exit early if there was only one branch to fetch.
3014 return
3015
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003016 changes_to_fetch = changes[1:]
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003017 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003018 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003019 if max_processes is not None
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003020 else len(changes_to_fetch))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003021
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003022 fetched_cls = set()
3023 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003024 while True:
3025 try:
3026 row = it.next(timeout=5)
3027 except multiprocessing.TimeoutError:
3028 break
3029
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003030 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003031 yield row
3032
3033 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003034 for cl in set(changes_to_fetch) - fetched_cls:
3035 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003036
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003037 else:
3038 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003039 for cl in changes:
3040 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003041
rmistry@google.com2dd99862015-06-22 12:22:18 +00003042
3043def upload_branch_deps(cl, args):
3044 """Uploads CLs of local branches that are dependents of the current branch.
3045
3046 If the local branch dependency tree looks like:
3047 test1 -> test2.1 -> test3.1
3048 -> test3.2
3049 -> test2.2 -> test3.3
3050
3051 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3052 run on the dependent branches in this order:
3053 test2.1, test3.1, test3.2, test2.2, test3.3
3054
3055 Note: This function does not rebase your local dependent branches. Use it when
3056 you make a change to the parent branch that will not conflict with its
3057 dependent branches, and you would like their dependencies updated in
3058 Rietveld.
3059 """
3060 if git_common.is_dirty_git_tree('upload-branch-deps'):
3061 return 1
3062
3063 root_branch = cl.GetBranch()
3064 if root_branch is None:
3065 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3066 'Get on a branch!')
3067 if not cl.GetIssue() or not cl.GetPatchset():
3068 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3069 'patchset dependencies without an uploaded CL.')
3070
3071 branches = RunGit(['for-each-ref',
3072 '--format=%(refname:short) %(upstream:short)',
3073 'refs/heads'])
3074 if not branches:
3075 print('No local branches found.')
3076 return 0
3077
3078 # Create a dictionary of all local branches to the branches that are dependent
3079 # on it.
3080 tracked_to_dependents = collections.defaultdict(list)
3081 for b in branches.splitlines():
3082 tokens = b.split()
3083 if len(tokens) == 2:
3084 branch_name, tracked = tokens
3085 tracked_to_dependents[tracked].append(branch_name)
3086
vapiera7fbd5a2016-06-16 09:17:49 -07003087 print()
3088 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003089 dependents = []
3090 def traverse_dependents_preorder(branch, padding=''):
3091 dependents_to_process = tracked_to_dependents.get(branch, [])
3092 padding += ' '
3093 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003094 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003095 dependents.append(dependent)
3096 traverse_dependents_preorder(dependent, padding)
3097 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003098 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003099
3100 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003101 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003102 return 0
3103
vapiera7fbd5a2016-06-16 09:17:49 -07003104 print('This command will checkout all dependent branches and run '
3105 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003106 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3107
andybons@chromium.org962f9462016-02-03 20:00:42 +00003108 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003109 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003110 args.extend(['-t', 'Updated patchset dependency'])
3111
rmistry@google.com2dd99862015-06-22 12:22:18 +00003112 # Record all dependents that failed to upload.
3113 failures = {}
3114 # Go through all dependents, checkout the branch and upload.
3115 try:
3116 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003117 print()
3118 print('--------------------------------------')
3119 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003120 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003121 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003122 try:
3123 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003124 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003125 failures[dependent_branch] = 1
3126 except: # pylint: disable=W0702
3127 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003128 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003129 finally:
3130 # Swap back to the original root branch.
3131 RunGit(['checkout', '-q', root_branch])
3132
vapiera7fbd5a2016-06-16 09:17:49 -07003133 print()
3134 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003135 for dependent_branch in dependents:
3136 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003137 print(' %s : %s' % (dependent_branch, upload_status))
3138 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003139
3140 return 0
3141
3142
kmarshall3bff56b2016-06-06 18:31:47 -07003143def CMDarchive(parser, args):
3144 """Archives and deletes branches associated with closed changelists."""
3145 parser.add_option(
3146 '-j', '--maxjobs', action='store', type=int,
3147 help='The maximum number of jobs to use when retrieving review status')
3148 parser.add_option(
3149 '-f', '--force', action='store_true',
3150 help='Bypasses the confirmation prompt.')
3151
3152 auth.add_auth_options(parser)
3153 options, args = parser.parse_args(args)
3154 if args:
3155 parser.error('Unsupported args: %s' % ' '.join(args))
3156 auth_config = auth.extract_auth_config_from_options(options)
3157
3158 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3159 if not branches:
3160 return 0
3161
vapiera7fbd5a2016-06-16 09:17:49 -07003162 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003163 changes = [Changelist(branchref=b, auth_config=auth_config)
3164 for b in branches.splitlines()]
3165 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3166 statuses = get_cl_statuses(changes,
3167 fine_grained=True,
3168 max_processes=options.maxjobs)
3169 proposal = [(cl.GetBranch(),
3170 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3171 for cl, status in statuses
3172 if status == 'closed']
3173 proposal.sort()
3174
3175 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003176 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003177 return 0
3178
3179 current_branch = GetCurrentBranch()
3180
vapiera7fbd5a2016-06-16 09:17:49 -07003181 print('\nBranches with closed issues that will be archived:\n')
3182 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
kmarshall3bff56b2016-06-06 18:31:47 -07003183 for next_item in proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003184 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003185
3186 if any(branch == current_branch for branch, _ in proposal):
3187 print('You are currently on a branch \'%s\' which is associated with a '
3188 'closed codereview issue, so archive cannot proceed. Please '
3189 'checkout another branch and run this command again.' %
3190 current_branch)
3191 return 1
3192
3193 if not options.force:
3194 if ask_for_data('\nProceed with deletion (Y/N)? ').lower() != 'y':
vapiera7fbd5a2016-06-16 09:17:49 -07003195 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003196 return 1
3197
3198 for branch, tagname in proposal:
3199 RunGit(['tag', tagname, branch])
3200 RunGit(['branch', '-D', branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003201 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003202
3203 return 0
3204
3205
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003206def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003207 """Show status of changelists.
3208
3209 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003210 - Red not sent for review or broken
3211 - Blue waiting for review
3212 - Yellow waiting for you to reply to review
3213 - Green LGTM'ed
3214 - Magenta in the commit queue
3215 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003216
3217 Also see 'git cl comments'.
3218 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003219 parser.add_option('--field',
3220 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003221 parser.add_option('-f', '--fast', action='store_true',
3222 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003223 parser.add_option(
3224 '-j', '--maxjobs', action='store', type=int,
3225 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003226
3227 auth.add_auth_options(parser)
3228 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003229 if args:
3230 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003231 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003232
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003233 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003234 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003235 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003236 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003237 elif options.field == 'id':
3238 issueid = cl.GetIssue()
3239 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003240 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003241 elif options.field == 'patch':
3242 patchset = cl.GetPatchset()
3243 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003244 print(patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003245 elif options.field == 'url':
3246 url = cl.GetIssueURL()
3247 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003248 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003249 return 0
3250
3251 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3252 if not branches:
3253 print('No local branch found.')
3254 return 0
3255
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003256 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003257 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003258 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003259 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003260 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003261 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003262 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003263
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003264 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003265 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3266 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3267 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003268 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003269 c, status = output.next()
3270 branch_statuses[c.GetBranch()] = status
3271 status = branch_statuses.pop(branch)
3272 url = cl.GetIssueURL()
3273 if url and (not status or status == 'error'):
3274 # The issue probably doesn't exist anymore.
3275 url += ' (broken)'
3276
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003277 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003278 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003279 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003280 color = ''
3281 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003282 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003283 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003284 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003285 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003286
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003287 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003288 print()
3289 print('Current branch:',)
3290 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003291 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003292 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003293 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003294 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003295 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003296 print('Issue description:')
3297 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003298 return 0
3299
3300
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003301def colorize_CMDstatus_doc():
3302 """To be called once in main() to add colors to git cl status help."""
3303 colors = [i for i in dir(Fore) if i[0].isupper()]
3304
3305 def colorize_line(line):
3306 for color in colors:
3307 if color in line.upper():
3308 # Extract whitespaces first and the leading '-'.
3309 indent = len(line) - len(line.lstrip(' ')) + 1
3310 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3311 return line
3312
3313 lines = CMDstatus.__doc__.splitlines()
3314 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3315
3316
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003317@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003318def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003319 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003320
3321 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003322 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003323 parser.add_option('-r', '--reverse', action='store_true',
3324 help='Lookup the branch(es) for the specified issues. If '
3325 'no issues are specified, all branches with mapped '
3326 'issues will be listed.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003327 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003328 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003329 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003330
dnj@chromium.org406c4402015-03-03 17:22:28 +00003331 if options.reverse:
3332 branches = RunGit(['for-each-ref', 'refs/heads',
3333 '--format=%(refname:short)']).splitlines()
3334
3335 # Reverse issue lookup.
3336 issue_branch_map = {}
3337 for branch in branches:
3338 cl = Changelist(branchref=branch)
3339 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3340 if not args:
3341 args = sorted(issue_branch_map.iterkeys())
3342 for issue in args:
3343 if not issue:
3344 continue
vapiera7fbd5a2016-06-16 09:17:49 -07003345 print('Branch for issue number %s: %s' % (
3346 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
dnj@chromium.org406c4402015-03-03 17:22:28 +00003347 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003348 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003349 if len(args) > 0:
3350 try:
3351 issue = int(args[0])
3352 except ValueError:
3353 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003354 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003355 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003356 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003357 return 0
3358
3359
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003360def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003361 """Shows or posts review comments for any changelist."""
3362 parser.add_option('-a', '--add-comment', dest='comment',
3363 help='comment to add to an issue')
3364 parser.add_option('-i', dest='issue',
3365 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003366 parser.add_option('-j', '--json-file',
3367 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003368 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003369 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003370 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003371
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003372 issue = None
3373 if options.issue:
3374 try:
3375 issue = int(options.issue)
3376 except ValueError:
3377 DieWithError('A review issue id is expected to be a number')
3378
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003379 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003380
3381 if options.comment:
3382 cl.AddComment(options.comment)
3383 return 0
3384
3385 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003386 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003387 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003388 summary.append({
3389 'date': message['date'],
3390 'lgtm': False,
3391 'message': message['text'],
3392 'not_lgtm': False,
3393 'sender': message['sender'],
3394 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003395 if message['disapproval']:
3396 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003397 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003398 elif message['approval']:
3399 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003400 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003401 elif message['sender'] == data['owner_email']:
3402 color = Fore.MAGENTA
3403 else:
3404 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003405 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003406 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003407 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003408 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003409 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003410 if options.json_file:
3411 with open(options.json_file, 'wb') as f:
3412 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003413 return 0
3414
3415
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003416@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003417def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003418 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003419 parser.add_option('-d', '--display', action='store_true',
3420 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003421 parser.add_option('-n', '--new-description',
3422 help='New description to set for this issue (- for stdin)')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003423
3424 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003425 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003426 options, args = parser.parse_args(args)
3427 _process_codereview_select_options(parser, options)
3428
3429 target_issue = None
3430 if len(args) > 0:
3431 issue_arg = ParseIssueNumberArgument(args[0])
3432 if not issue_arg.valid:
3433 parser.print_help()
3434 return 1
3435 target_issue = issue_arg.issue
3436
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003437 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003438
3439 cl = Changelist(
3440 auth_config=auth_config, issue=target_issue,
3441 codereview=options.forced_codereview)
3442
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003443 if not cl.GetIssue():
3444 DieWithError('This branch has no associated changelist.')
3445 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003446
smut@google.com34fb6b12015-07-13 20:03:26 +00003447 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003448 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003449 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003450
3451 if options.new_description:
3452 text = options.new_description
3453 if text == '-':
3454 text = '\n'.join(l.rstrip() for l in sys.stdin)
3455
3456 description.set_description(text)
3457 else:
3458 description.prompt()
3459
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003460 if cl.GetDescription() != description.description:
3461 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003462 return 0
3463
3464
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003465def CreateDescriptionFromLog(args):
3466 """Pulls out the commit log to use as a base for the CL description."""
3467 log_args = []
3468 if len(args) == 1 and not args[0].endswith('.'):
3469 log_args = [args[0] + '..']
3470 elif len(args) == 1 and args[0].endswith('...'):
3471 log_args = [args[0][:-1]]
3472 elif len(args) == 2:
3473 log_args = [args[0] + '..' + args[1]]
3474 else:
3475 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003476 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003477
3478
thestig@chromium.org44202a22014-03-11 19:22:18 +00003479def CMDlint(parser, args):
3480 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003481 parser.add_option('--filter', action='append', metavar='-x,+y',
3482 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003483 auth.add_auth_options(parser)
3484 options, args = parser.parse_args(args)
3485 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003486
3487 # Access to a protected member _XX of a client class
3488 # pylint: disable=W0212
3489 try:
3490 import cpplint
3491 import cpplint_chromium
3492 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003493 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003494 return 1
3495
3496 # Change the current working directory before calling lint so that it
3497 # shows the correct base.
3498 previous_cwd = os.getcwd()
3499 os.chdir(settings.GetRoot())
3500 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003501 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003502 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3503 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003504 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003505 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003506 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003507
3508 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003509 command = args + files
3510 if options.filter:
3511 command = ['--filter=' + ','.join(options.filter)] + command
3512 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003513
3514 white_regex = re.compile(settings.GetLintRegex())
3515 black_regex = re.compile(settings.GetLintIgnoreRegex())
3516 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3517 for filename in filenames:
3518 if white_regex.match(filename):
3519 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07003520 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003521 else:
3522 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3523 extra_check_functions)
3524 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003525 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003526 finally:
3527 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07003528 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003529 if cpplint._cpplint_state.error_count != 0:
3530 return 1
3531 return 0
3532
3533
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003534def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003535 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003536 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003537 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003538 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003539 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003540 auth.add_auth_options(parser)
3541 options, args = parser.parse_args(args)
3542 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003543
sbc@chromium.org71437c02015-04-09 19:29:40 +00003544 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07003545 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003546 return 1
3547
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003548 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003549 if args:
3550 base_branch = args[0]
3551 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003552 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003553 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003554
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003555 cl.RunHook(
3556 committing=not options.upload,
3557 may_prompt=False,
3558 verbose=options.verbose,
3559 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003560 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003561
3562
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003563def GenerateGerritChangeId(message):
3564 """Returns Ixxxxxx...xxx change id.
3565
3566 Works the same way as
3567 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3568 but can be called on demand on all platforms.
3569
3570 The basic idea is to generate git hash of a state of the tree, original commit
3571 message, author/committer info and timestamps.
3572 """
3573 lines = []
3574 tree_hash = RunGitSilent(['write-tree'])
3575 lines.append('tree %s' % tree_hash.strip())
3576 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3577 if code == 0:
3578 lines.append('parent %s' % parent.strip())
3579 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3580 lines.append('author %s' % author.strip())
3581 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3582 lines.append('committer %s' % committer.strip())
3583 lines.append('')
3584 # Note: Gerrit's commit-hook actually cleans message of some lines and
3585 # whitespace. This code is not doing this, but it clearly won't decrease
3586 # entropy.
3587 lines.append(message)
3588 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3589 stdin='\n'.join(lines))
3590 return 'I%s' % change_hash.strip()
3591
3592
wittman@chromium.org455dc922015-01-26 20:15:50 +00003593def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3594 """Computes the remote branch ref to use for the CL.
3595
3596 Args:
3597 remote (str): The git remote for the CL.
3598 remote_branch (str): The git remote branch for the CL.
3599 target_branch (str): The target branch specified by the user.
3600 pending_prefix (str): The pending prefix from the settings.
3601 """
3602 if not (remote and remote_branch):
3603 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003604
wittman@chromium.org455dc922015-01-26 20:15:50 +00003605 if target_branch:
3606 # Cannonicalize branch references to the equivalent local full symbolic
3607 # refs, which are then translated into the remote full symbolic refs
3608 # below.
3609 if '/' not in target_branch:
3610 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3611 else:
3612 prefix_replacements = (
3613 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3614 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3615 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3616 )
3617 match = None
3618 for regex, replacement in prefix_replacements:
3619 match = re.search(regex, target_branch)
3620 if match:
3621 remote_branch = target_branch.replace(match.group(0), replacement)
3622 break
3623 if not match:
3624 # This is a branch path but not one we recognize; use as-is.
3625 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003626 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3627 # Handle the refs that need to land in different refs.
3628 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003629
wittman@chromium.org455dc922015-01-26 20:15:50 +00003630 # Create the true path to the remote branch.
3631 # Does the following translation:
3632 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3633 # * refs/remotes/origin/master -> refs/heads/master
3634 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3635 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3636 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3637 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3638 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3639 'refs/heads/')
3640 elif remote_branch.startswith('refs/remotes/branch-heads'):
3641 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3642 # If a pending prefix exists then replace refs/ with it.
3643 if pending_prefix:
3644 remote_branch = remote_branch.replace('refs/', pending_prefix)
3645 return remote_branch
3646
3647
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003648def cleanup_list(l):
3649 """Fixes a list so that comma separated items are put as individual items.
3650
3651 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3652 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3653 """
3654 items = sum((i.split(',') for i in l), [])
3655 stripped_items = (i.strip() for i in items)
3656 return sorted(filter(None, stripped_items))
3657
3658
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003659@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003660def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003661 """Uploads the current changelist to codereview.
3662
3663 Can skip dependency patchset uploads for a branch by running:
3664 git config branch.branch_name.skip-deps-uploads True
3665 To unset run:
3666 git config --unset branch.branch_name.skip-deps-uploads
3667 Can also set the above globally by using the --global flag.
3668 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003669 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3670 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003671 parser.add_option('--bypass-watchlists', action='store_true',
3672 dest='bypass_watchlists',
3673 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003674 parser.add_option('-f', action='store_true', dest='force',
3675 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003676 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003677 parser.add_option('-t', dest='title',
3678 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003679 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003680 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003681 help='reviewer email addresses')
3682 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003683 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003684 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003685 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003686 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003687 parser.add_option('--emulate_svn_auto_props',
3688 '--emulate-svn-auto-props',
3689 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003690 dest="emulate_svn_auto_props",
3691 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003692 parser.add_option('-c', '--use-commit-queue', action='store_true',
3693 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003694 parser.add_option('--private', action='store_true',
3695 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003696 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003697 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003698 metavar='TARGET',
3699 help='Apply CL to remote ref TARGET. ' +
3700 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003701 parser.add_option('--squash', action='store_true',
3702 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003703 parser.add_option('--no-squash', action='store_true',
3704 help='Don\'t squash multiple commits into one ' +
3705 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003706 parser.add_option('--email', default=None,
3707 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003708 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3709 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003710 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3711 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003712 help='Send the patchset to do a CQ dry run right after '
3713 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003714 parser.add_option('--dependencies', action='store_true',
3715 help='Uploads CLs of all the local branches that depend on '
3716 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003717
rmistry@google.com2dd99862015-06-22 12:22:18 +00003718 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003719 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003720 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003721 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003722 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003723 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003724 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003725
sbc@chromium.org71437c02015-04-09 19:29:40 +00003726 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003727 return 1
3728
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003729 options.reviewers = cleanup_list(options.reviewers)
3730 options.cc = cleanup_list(options.cc)
3731
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003732 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3733 settings.GetIsGerrit()
3734
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003735 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00003736 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003737
3738
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003739def IsSubmoduleMergeCommit(ref):
3740 # When submodules are added to the repo, we expect there to be a single
3741 # non-git-svn merge commit at remote HEAD with a signature comment.
3742 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003743 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003744 return RunGit(cmd) != ''
3745
3746
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003747def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003748 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003749
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003750 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3751 upstream and closes the issue automatically and atomically.
3752
3753 Otherwise (in case of Rietveld):
3754 Squashes branch into a single commit.
3755 Updates changelog with metadata (e.g. pointer to review).
3756 Pushes/dcommits the code upstream.
3757 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003758 """
3759 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3760 help='bypass upload presubmit hook')
3761 parser.add_option('-m', dest='message',
3762 help="override review description")
3763 parser.add_option('-f', action='store_true', dest='force',
3764 help="force yes to questions (don't prompt)")
3765 parser.add_option('-c', dest='contributor',
3766 help="external contributor for patch (appended to " +
3767 "description and used as author for git). Should be " +
3768 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003769 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003770 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003771 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003772 auth_config = auth.extract_auth_config_from_options(options)
3773
3774 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003775
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003776 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3777 if cl.IsGerrit():
3778 if options.message:
3779 # This could be implemented, but it requires sending a new patch to
3780 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3781 # Besides, Gerrit has the ability to change the commit message on submit
3782 # automatically, thus there is no need to support this option (so far?).
3783 parser.error('-m MESSAGE option is not supported for Gerrit.')
3784 if options.contributor:
3785 parser.error(
3786 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3787 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3788 'the contributor\'s "name <email>". If you can\'t upload such a '
3789 'commit for review, contact your repository admin and request'
3790 '"Forge-Author" permission.')
3791 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3792 options.verbose)
3793
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003794 current = cl.GetBranch()
3795 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3796 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07003797 print()
3798 print('Attempting to push branch %r into another local branch!' % current)
3799 print()
3800 print('Either reparent this branch on top of origin/master:')
3801 print(' git reparent-branch --root')
3802 print()
3803 print('OR run `git rebase-update` if you think the parent branch is ')
3804 print('already committed.')
3805 print()
3806 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003807 return 1
3808
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003809 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003810 # Default to merging against our best guess of the upstream branch.
3811 args = [cl.GetUpstreamBranch()]
3812
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003813 if options.contributor:
3814 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07003815 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003816 return 1
3817
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003818 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003819 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003820
sbc@chromium.org71437c02015-04-09 19:29:40 +00003821 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003822 return 1
3823
3824 # This rev-list syntax means "show all commits not in my branch that
3825 # are in base_branch".
3826 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3827 base_branch]).splitlines()
3828 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07003829 print('Base branch "%s" has %d commits '
3830 'not in this branch.' % (base_branch, len(upstream_commits)))
3831 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003832 return 1
3833
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003834 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003835 svn_head = None
3836 if cmd == 'dcommit' or base_has_submodules:
3837 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
3838 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003839
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003840 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003841 # If the base_head is a submodule merge commit, the first parent of the
3842 # base_head should be a git-svn commit, which is what we're interested in.
3843 base_svn_head = base_branch
3844 if base_has_submodules:
3845 base_svn_head += '^1'
3846
3847 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003848 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07003849 print('This branch has %d additional commits not upstreamed yet.'
3850 % len(extra_commits.splitlines()))
3851 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
3852 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003853 return 1
3854
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003855 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003856 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003857 author = None
3858 if options.contributor:
3859 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003860 hook_results = cl.RunHook(
3861 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003862 may_prompt=not options.force,
3863 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003864 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003865 if not hook_results.should_continue():
3866 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003867
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003868 # Check the tree status if the tree status URL is set.
3869 status = GetTreeStatus()
3870 if 'closed' == status:
3871 print('The tree is closed. Please wait for it to reopen. Use '
3872 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3873 return 1
3874 elif 'unknown' == status:
3875 print('Unable to determine tree status. Please verify manually and '
3876 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3877 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003878
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003879 change_desc = ChangeDescription(options.message)
3880 if not change_desc.description and cl.GetIssue():
3881 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003882
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003883 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00003884 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003885 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00003886 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003887 print('No description set.')
3888 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00003889 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003890
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003891 # Keep a separate copy for the commit message, because the commit message
3892 # contains the link to the Rietveld issue, while the Rietveld message contains
3893 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003894 # Keep a separate copy for the commit message.
3895 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00003896 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003897
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003898 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00003899 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00003900 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00003901 # after it. Add a period on a new line to circumvent this. Also add a space
3902 # before the period to make sure that Gitiles continues to correctly resolve
3903 # the URL.
3904 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003905 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003906 commit_desc.append_footer('Patch from %s.' % options.contributor)
3907
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00003908 print('Description:')
3909 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003910
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003911 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003912 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00003913 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003914
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003915 # We want to squash all this branch's commits into one commit with the proper
3916 # description. We do this by doing a "reset --soft" to the base branch (which
3917 # keeps the working copy the same), then dcommitting that. If origin/master
3918 # has a submodule merge commit, we'll also need to cherry-pick the squashed
3919 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003920 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003921 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
3922 # Delete the branches if they exist.
3923 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
3924 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
3925 result = RunGitWithCode(showref_cmd)
3926 if result[0] == 0:
3927 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003928
3929 # We might be in a directory that's present in this branch but not in the
3930 # trunk. Move up to the top of the tree so that git commands that expect a
3931 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003932 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003933 if rel_base_path:
3934 os.chdir(rel_base_path)
3935
3936 # Stuff our change into the merge branch.
3937 # We wrap in a try...finally block so if anything goes wrong,
3938 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003939 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003940 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003941 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003942 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003943 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00003944 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003945 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003946 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003947 RunGit(
3948 [
3949 'commit', '--author', options.contributor,
3950 '-m', commit_desc.description,
3951 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003952 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003953 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003954 if base_has_submodules:
3955 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
3956 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
3957 RunGit(['checkout', CHERRY_PICK_BRANCH])
3958 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003959 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003960 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003961 mirror = settings.GetGitMirror(remote)
3962 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003963 pending_prefix = settings.GetPendingRefPrefix()
3964 if not pending_prefix or branch.startswith(pending_prefix):
3965 # If not using refs/pending/heads/* at all, or target ref is already set
3966 # to pending, then push to the target ref directly.
3967 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003968 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003969 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003970 else:
3971 # Cherry-pick the change on top of pending ref and then push it.
3972 assert branch.startswith('refs/'), branch
3973 assert pending_prefix[-1] == '/', pending_prefix
3974 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003975 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003976 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003977 if retcode == 0:
3978 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003979 else:
3980 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003981 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003982 'svn', 'dcommit',
3983 '-C%s' % options.similarity,
3984 '--no-rebase', '--rmdir',
3985 ]
3986 if settings.GetForceHttpsCommitUrl():
3987 # Allow forcing https commit URLs for some projects that don't allow
3988 # committing to http URLs (like Google Code).
3989 remote_url = cl.GetGitSvnRemoteUrl()
3990 if urlparse.urlparse(remote_url).scheme == 'http':
3991 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003992 cmd_args.append('--commit-url=%s' % remote_url)
3993 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003994 if 'Committed r' in output:
3995 revision = re.match(
3996 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
3997 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003998 finally:
3999 # And then swap back to the original branch and clean up.
4000 RunGit(['checkout', '-q', cl.GetBranch()])
4001 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004002 if base_has_submodules:
4003 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004004
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004005 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004006 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004007 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004008
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004009 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004010 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004011 try:
4012 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4013 # We set pushed_to_pending to False, since it made it all the way to the
4014 # real ref.
4015 pushed_to_pending = False
4016 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004017 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004018
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004019 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004020 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004021 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004022 if not to_pending:
4023 if viewvc_url and revision:
4024 change_desc.append_footer(
4025 'Committed: %s%s' % (viewvc_url, revision))
4026 elif revision:
4027 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004028 print('Closing issue '
4029 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004030 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004031 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004032 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004033 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004034 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004035 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004036 if options.bypass_hooks:
4037 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4038 else:
4039 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004040 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004041 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004042
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004043 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004044 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004045 print('The commit is in the pending queue (%s).' % pending_ref)
4046 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4047 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004048
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004049 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4050 if os.path.isfile(hook):
4051 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004052
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004053 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004054
4055
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004056def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004057 print()
4058 print('Waiting for commit to be landed on %s...' % real_ref)
4059 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004060 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4061 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004062 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004063
4064 loop = 0
4065 while True:
4066 sys.stdout.write('fetching (%d)... \r' % loop)
4067 sys.stdout.flush()
4068 loop += 1
4069
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004070 if mirror:
4071 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004072 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4073 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4074 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4075 for commit in commits.splitlines():
4076 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004077 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004078 return commit
4079
4080 current_rev = to_rev
4081
4082
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004083def PushToGitPending(remote, pending_ref, upstream_ref):
4084 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4085
4086 Returns:
4087 (retcode of last operation, output log of last operation).
4088 """
4089 assert pending_ref.startswith('refs/'), pending_ref
4090 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4091 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4092 code = 0
4093 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004094 max_attempts = 3
4095 attempts_left = max_attempts
4096 while attempts_left:
4097 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004098 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004099 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004100
4101 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004102 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004103 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004104 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004105 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004106 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004107 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004108 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004109 continue
4110
4111 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004112 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004113 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004114 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004115 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004116 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4117 'the following files have merge conflicts:' % pending_ref)
4118 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4119 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004120 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004121 return code, out
4122
4123 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004124 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004125 code, out = RunGitWithCode(
4126 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4127 if code == 0:
4128 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004129 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004130 return code, out
4131
vapiera7fbd5a2016-06-16 09:17:49 -07004132 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004133 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004134 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004135 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004136 print('Fatal push error. Make sure your .netrc credentials and git '
4137 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004138 return code, out
4139
vapiera7fbd5a2016-06-16 09:17:49 -07004140 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004141 return code, out
4142
4143
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004144def IsFatalPushFailure(push_stdout):
4145 """True if retrying push won't help."""
4146 return '(prohibited by Gerrit)' in push_stdout
4147
4148
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004149@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004150def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004151 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004152 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004153 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004154 # If it looks like previous commits were mirrored with git-svn.
4155 message = """This repository appears to be a git-svn mirror, but no
4156upstream SVN master is set. You probably need to run 'git auto-svn' once."""
4157 else:
4158 message = """This doesn't appear to be an SVN repository.
4159If your project has a true, writeable git repository, you probably want to run
4160'git cl land' instead.
4161If your project has a git mirror of an upstream SVN master, you probably need
4162to run 'git svn init'.
4163
4164Using the wrong command might cause your commit to appear to succeed, and the
4165review to be closed, without actually landing upstream. If you choose to
4166proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004167 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004168 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004169 return SendUpstream(parser, args, 'dcommit')
4170
4171
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004172@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004173def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004174 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004175 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004176 print('This appears to be an SVN repository.')
4177 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004178 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004179 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004180 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004181
4182
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004183@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004184def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004185 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004186 parser.add_option('-b', dest='newbranch',
4187 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004188 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004189 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004190 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4191 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004192 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004193 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004194 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004195 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004196 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004197 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004198
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004199
4200 group = optparse.OptionGroup(
4201 parser,
4202 'Options for continuing work on the current issue uploaded from a '
4203 'different clone (e.g. different machine). Must be used independently '
4204 'from the other options. No issue number should be specified, and the '
4205 'branch must have an issue number associated with it')
4206 group.add_option('--reapply', action='store_true', dest='reapply',
4207 help='Reset the branch and reapply the issue.\n'
4208 'CAUTION: This will undo any local changes in this '
4209 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004210
4211 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004212 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004213 parser.add_option_group(group)
4214
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004215 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004216 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004217 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004218 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004219 auth_config = auth.extract_auth_config_from_options(options)
4220
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004221
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004222 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004223 if options.newbranch:
4224 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004225 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004226 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004227
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004228 cl = Changelist(auth_config=auth_config,
4229 codereview=options.forced_codereview)
4230 if not cl.GetIssue():
4231 parser.error('current branch must have an associated issue')
4232
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004233 upstream = cl.GetUpstreamBranch()
4234 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004235 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004236
4237 RunGit(['reset', '--hard', upstream])
4238 if options.pull:
4239 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004240
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004241 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4242 options.directory)
4243
4244 if len(args) != 1 or not args[0]:
4245 parser.error('Must specify issue number or url')
4246
4247 # We don't want uncommitted changes mixed up with the patch.
4248 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004249 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004250
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004251 if options.newbranch:
4252 if options.force:
4253 RunGit(['branch', '-D', options.newbranch],
4254 stderr=subprocess2.PIPE, error_ok=True)
4255 RunGit(['new-branch', options.newbranch])
4256
4257 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4258
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004259 if cl.IsGerrit():
4260 if options.reject:
4261 parser.error('--reject is not supported with Gerrit codereview.')
4262 if options.nocommit:
4263 parser.error('--nocommit is not supported with Gerrit codereview.')
4264 if options.directory:
4265 parser.error('--directory is not supported with Gerrit codereview.')
4266
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004267 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004268 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004269
4270
4271def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004272 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004273 # Provide a wrapper for git svn rebase to help avoid accidental
4274 # git svn dcommit.
4275 # It's the only command that doesn't use parser at all since we just defer
4276 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004277
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004278 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004279
4280
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004281def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004282 """Fetches the tree status and returns either 'open', 'closed',
4283 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004284 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004285 if url:
4286 status = urllib2.urlopen(url).read().lower()
4287 if status.find('closed') != -1 or status == '0':
4288 return 'closed'
4289 elif status.find('open') != -1 or status == '1':
4290 return 'open'
4291 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004292 return 'unset'
4293
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004294
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004295def GetTreeStatusReason():
4296 """Fetches the tree status from a json url and returns the message
4297 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004298 url = settings.GetTreeStatusUrl()
4299 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004300 connection = urllib2.urlopen(json_url)
4301 status = json.loads(connection.read())
4302 connection.close()
4303 return status['message']
4304
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004305
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004306def GetBuilderMaster(bot_list):
4307 """For a given builder, fetch the master from AE if available."""
4308 map_url = 'https://builders-map.appspot.com/'
4309 try:
4310 master_map = json.load(urllib2.urlopen(map_url))
4311 except urllib2.URLError as e:
4312 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4313 (map_url, e))
4314 except ValueError as e:
4315 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4316 if not master_map:
4317 return None, 'Failed to build master map.'
4318
4319 result_master = ''
4320 for bot in bot_list:
4321 builder = bot.split(':', 1)[0]
4322 master_list = master_map.get(builder, [])
4323 if not master_list:
4324 return None, ('No matching master for builder %s.' % builder)
4325 elif len(master_list) > 1:
4326 return None, ('The builder name %s exists in multiple masters %s.' %
4327 (builder, master_list))
4328 else:
4329 cur_master = master_list[0]
4330 if not result_master:
4331 result_master = cur_master
4332 elif result_master != cur_master:
4333 return None, 'The builders do not belong to the same master.'
4334 return result_master, None
4335
4336
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004337def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004338 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004339 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004340 status = GetTreeStatus()
4341 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004342 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004343 return 2
4344
vapiera7fbd5a2016-06-16 09:17:49 -07004345 print('The tree is %s' % status)
4346 print()
4347 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004348 if status != 'open':
4349 return 1
4350 return 0
4351
4352
maruel@chromium.org15192402012-09-06 12:38:29 +00004353def CMDtry(parser, args):
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004354 """Triggers try jobs through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00004355 group = optparse.OptionGroup(parser, "Try job options")
4356 group.add_option(
4357 "-b", "--bot", action="append",
4358 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
4359 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004360 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00004361 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004362 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00004363 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004364 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00004365 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004366 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004367 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004368 "-r", "--revision",
4369 help="Revision to use for the try job; default: the "
4370 "revision will be determined by the try server; see "
4371 "its waterfall for more info")
4372 group.add_option(
4373 "-c", "--clobber", action="store_true", default=False,
4374 help="Force a clobber before building; e.g. don't do an "
4375 "incremental build")
4376 group.add_option(
4377 "--project",
4378 help="Override which project to use. Projects are defined "
4379 "server-side to define what default bot set to use")
4380 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004381 "-p", "--property", dest="properties", action="append", default=[],
4382 help="Specify generic properties in the form -p key1=value1 -p "
4383 "key2=value2 etc (buildbucket only). The value will be treated as "
4384 "json if decodable, or as string otherwise.")
4385 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004386 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004387 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004388 "--use-rietveld", action="store_true", default=False,
4389 help="Use Rietveld to trigger try jobs.")
4390 group.add_option(
4391 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4392 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004393 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004394 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004395 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004396 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004397
machenbach@chromium.org45453142015-09-15 08:45:22 +00004398 if options.use_rietveld and options.properties:
4399 parser.error('Properties can only be specified with buildbucket')
4400
4401 # Make sure that all properties are prop=value pairs.
4402 bad_params = [x for x in options.properties if '=' not in x]
4403 if bad_params:
4404 parser.error('Got properties with missing "=": %s' % bad_params)
4405
maruel@chromium.org15192402012-09-06 12:38:29 +00004406 if args:
4407 parser.error('Unknown arguments: %s' % args)
4408
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004409 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004410 if not cl.GetIssue():
4411 parser.error('Need to upload first')
4412
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004413 if cl.IsGerrit():
4414 parser.error(
4415 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4416 'If your project has Commit Queue, dry run is a workaround:\n'
4417 ' git cl set-commit --dry-run')
4418 # Code below assumes Rietveld issue.
4419 # TODO(tandrii): actually implement for Gerrit http://crbug.com/599931.
4420
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004421 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004422 if props.get('closed'):
4423 parser.error('Cannot send tryjobs for a closed CL')
4424
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004425 if props.get('private'):
4426 parser.error('Cannot use trybots with private issue')
4427
maruel@chromium.org15192402012-09-06 12:38:29 +00004428 if not options.name:
4429 options.name = cl.GetBranch()
4430
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004431 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004432 options.master, err_msg = GetBuilderMaster(options.bot)
4433 if err_msg:
4434 parser.error('Tryserver master cannot be found because: %s\n'
4435 'Please manually specify the tryserver master'
4436 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004437
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004438 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004439 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004440 if not options.bot:
4441 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004442
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004443 # Get try masters from PRESUBMIT.py files.
4444 masters = presubmit_support.DoGetTryMasters(
4445 change,
4446 change.LocalPaths(),
4447 settings.GetRoot(),
4448 None,
4449 None,
4450 options.verbose,
4451 sys.stdout)
4452 if masters:
4453 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004454
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004455 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4456 options.bot = presubmit_support.DoGetTrySlaves(
4457 change,
4458 change.LocalPaths(),
4459 settings.GetRoot(),
4460 None,
4461 None,
4462 options.verbose,
4463 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004464
4465 if not options.bot:
4466 # Get try masters from cq.cfg if any.
4467 # TODO(tandrii): some (but very few) projects store cq.cfg in different
4468 # location.
4469 cq_cfg = os.path.join(change.RepositoryRoot(),
4470 'infra', 'config', 'cq.cfg')
4471 if os.path.exists(cq_cfg):
4472 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00004473 cq_masters = commit_queue.get_master_builder_map(
4474 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004475 for master, builders in cq_masters.iteritems():
4476 for builder in builders:
4477 # Skip presubmit builders, because these will fail without LGTM.
machenbach@chromium.org2403e802016-04-29 12:34:42 +00004478 masters.setdefault(master, {})[builder] = ['defaulttests']
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004479 if masters:
tandriib93dd2b2016-06-07 08:03:08 -07004480 print('Loaded default bots from CQ config (%s)' % cq_cfg)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004481 return masters
tandriib93dd2b2016-06-07 08:03:08 -07004482 else:
4483 print('CQ config exists (%s) but has no try bots listed' % cq_cfg)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004484
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004485 if not options.bot:
4486 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00004487
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004488 builders_and_tests = {}
4489 # TODO(machenbach): The old style command-line options don't support
4490 # multiple try masters yet.
4491 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4492 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4493
4494 for bot in old_style:
4495 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004496 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004497 elif ',' in bot:
4498 parser.error('Specify one bot per --bot flag')
4499 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004500 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004501
4502 for bot, tests in new_style:
4503 builders_and_tests.setdefault(bot, []).extend(tests)
4504
4505 # Return a master map with one master to be backwards compatible. The
4506 # master name defaults to an empty string, which will cause the master
4507 # not to be set on rietveld (deprecated).
4508 return {options.master: builders_and_tests}
4509
4510 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004511
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004512 for builders in masters.itervalues():
4513 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004514 print('ERROR You are trying to send a job to a triggered bot. This type '
4515 'of bot requires an\ninitial job from a parent (usually a builder).'
4516 ' Instead send your job to the parent.\n'
4517 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004518 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004519
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004520 patchset = cl.GetMostRecentPatchset()
4521 if patchset and patchset != cl.GetPatchset():
4522 print(
4523 '\nWARNING Mismatch between local config and server. Did a previous '
4524 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4525 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004526 if options.luci:
4527 trigger_luci_job(cl, masters, options)
4528 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004529 try:
4530 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4531 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004532 print('ERROR: %s' % ex)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004533 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004534 except Exception as e:
4535 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
vapiera7fbd5a2016-06-16 09:17:49 -07004536 print('ERROR: Exception when trying to trigger tryjobs: %s\n%s' %
4537 (e, stacktrace))
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004538 return 1
4539 else:
4540 try:
4541 cl.RpcServer().trigger_distributed_try_jobs(
4542 cl.GetIssue(), patchset, options.name, options.clobber,
4543 options.revision, masters)
4544 except urllib2.HTTPError as e:
4545 if e.code == 404:
4546 print('404 from rietveld; '
4547 'did you mean to use "git try" instead of "git cl try"?')
4548 return 1
4549 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004550
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004551 for (master, builders) in sorted(masters.iteritems()):
4552 if master:
vapiera7fbd5a2016-06-16 09:17:49 -07004553 print('Master: %s' % master)
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004554 length = max(len(builder) for builder in builders)
4555 for builder in sorted(builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004556 print(' %*s: %s' % (length, builder, ','.join(builders[builder])))
maruel@chromium.org15192402012-09-06 12:38:29 +00004557 return 0
4558
4559
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004560def CMDtry_results(parser, args):
4561 group = optparse.OptionGroup(parser, "Try job results options")
4562 group.add_option(
4563 "-p", "--patchset", type=int, help="patchset number if not current.")
4564 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004565 "--print-master", action='store_true', help="print master name as well.")
4566 group.add_option(
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004567 "--color", action='store_true', default=setup_color.IS_TTY,
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004568 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004569 group.add_option(
4570 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4571 help="Host of buildbucket. The default host is %default.")
4572 parser.add_option_group(group)
4573 auth.add_auth_options(parser)
4574 options, args = parser.parse_args(args)
4575 if args:
4576 parser.error('Unrecognized args: %s' % ' '.join(args))
4577
4578 auth_config = auth.extract_auth_config_from_options(options)
4579 cl = Changelist(auth_config=auth_config)
4580 if not cl.GetIssue():
4581 parser.error('Need to upload first')
4582
4583 if not options.patchset:
4584 options.patchset = cl.GetMostRecentPatchset()
4585 if options.patchset and options.patchset != cl.GetPatchset():
4586 print(
4587 '\nWARNING Mismatch between local config and server. Did a previous '
4588 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4589 'Continuing using\npatchset %s.\n' % options.patchset)
4590 try:
4591 jobs = fetch_try_jobs(auth_config, cl, options)
4592 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004593 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004594 return 1
4595 except Exception as e:
4596 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
vapiera7fbd5a2016-06-16 09:17:49 -07004597 print('ERROR: Exception when trying to fetch tryjobs: %s\n%s' %
4598 (e, stacktrace))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004599 return 1
4600 print_tryjobs(options, jobs)
4601 return 0
4602
4603
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004604@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004605def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004606 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004607 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004608 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004609 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004610
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004611 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004612 if args:
4613 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004614 branch = cl.GetBranch()
4615 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004616 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004617 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004618
4619 # Clear configured merge-base, if there is one.
4620 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004621 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004622 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004623 return 0
4624
4625
thestig@chromium.org00858c82013-12-02 23:08:03 +00004626def CMDweb(parser, args):
4627 """Opens the current CL in the web browser."""
4628 _, args = parser.parse_args(args)
4629 if args:
4630 parser.error('Unrecognized args: %s' % ' '.join(args))
4631
4632 issue_url = Changelist().GetIssueURL()
4633 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004634 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004635 return 1
4636
4637 webbrowser.open(issue_url)
4638 return 0
4639
4640
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004641def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004642 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004643 parser.add_option('-d', '--dry-run', action='store_true',
4644 help='trigger in dry run mode')
4645 parser.add_option('-c', '--clear', action='store_true',
4646 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004647 auth.add_auth_options(parser)
4648 options, args = parser.parse_args(args)
4649 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004650 if args:
4651 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004652 if options.dry_run and options.clear:
4653 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4654
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004655 cl = Changelist(auth_config=auth_config)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004656 if options.clear:
4657 state = _CQState.CLEAR
4658 elif options.dry_run:
4659 state = _CQState.DRY_RUN
4660 else:
4661 state = _CQState.COMMIT
4662 if not cl.GetIssue():
4663 parser.error('Must upload the issue first')
4664 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004665 return 0
4666
4667
groby@chromium.org411034a2013-02-26 15:12:01 +00004668def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004669 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004670 auth.add_auth_options(parser)
4671 options, args = parser.parse_args(args)
4672 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004673 if args:
4674 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004675 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00004676 # Ensure there actually is an issue to close.
4677 cl.GetDescription()
4678 cl.CloseIssue()
4679 return 0
4680
4681
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004682def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004683 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004684 auth.add_auth_options(parser)
4685 options, args = parser.parse_args(args)
4686 auth_config = auth.extract_auth_config_from_options(options)
4687 if args:
4688 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004689
4690 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004691 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004692 # Staged changes would be committed along with the patch from last
4693 # upload, hence counted toward the "last upload" side in the final
4694 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004695 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004696 return 1
4697
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004698 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004699 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004700 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004701 if not issue:
4702 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004703 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004704 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004705
4706 # Create a new branch based on the merge-base
4707 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004708 # Clear cached branch in cl object, to avoid overwriting original CL branch
4709 # properties.
4710 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004711 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004712 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004713 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004714 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004715 return rtn
4716
wychen@chromium.org06928532015-02-03 02:11:29 +00004717 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004718 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00004719 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004720 finally:
4721 RunGit(['checkout', '-q', branch])
4722 RunGit(['branch', '-D', TMP_BRANCH])
4723
4724 return 0
4725
4726
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004727def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004728 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004729 parser.add_option(
4730 '--no-color',
4731 action='store_true',
4732 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004733 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004734 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004735 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004736
4737 author = RunGit(['config', 'user.email']).strip() or None
4738
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004739 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004740
4741 if args:
4742 if len(args) > 1:
4743 parser.error('Unknown args')
4744 base_branch = args[0]
4745 else:
4746 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004747 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004748
4749 change = cl.GetChange(base_branch, None)
4750 return owners_finder.OwnersFinder(
4751 [f.LocalPath() for f in
4752 cl.GetChange(base_branch, None).AffectedFiles()],
4753 change.RepositoryRoot(), author,
4754 fopen=file, os_path=os.path, glob=glob.glob,
4755 disable_color=options.no_color).run()
4756
4757
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004758def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004759 """Generates a diff command."""
4760 # Generate diff for the current branch's changes.
4761 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4762 upstream_commit, '--' ]
4763
4764 if args:
4765 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004766 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004767 diff_cmd.append(arg)
4768 else:
4769 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004770
4771 return diff_cmd
4772
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004773def MatchingFileType(file_name, extensions):
4774 """Returns true if the file name ends with one of the given extensions."""
4775 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004776
enne@chromium.org555cfe42014-01-29 18:21:39 +00004777@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004778def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004779 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00004780 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004781 GN_EXTS = ['.gn', '.gni']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004782 parser.add_option('--full', action='store_true',
4783 help='Reformat the full content of all touched files')
4784 parser.add_option('--dry-run', action='store_true',
4785 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004786 parser.add_option('--python', action='store_true',
4787 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004788 parser.add_option('--diff', action='store_true',
4789 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004790 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004791
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004792 # git diff generates paths against the root of the repository. Change
4793 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004794 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004795 if rel_base_path:
4796 os.chdir(rel_base_path)
4797
digit@chromium.org29e47272013-05-17 17:01:46 +00004798 # Grab the merge-base commit, i.e. the upstream commit of the current
4799 # branch when it was created or the last time it was rebased. This is
4800 # to cover the case where the user may have called "git fetch origin",
4801 # moving the origin branch to a newer commit, but hasn't rebased yet.
4802 upstream_commit = None
4803 cl = Changelist()
4804 upstream_branch = cl.GetUpstreamBranch()
4805 if upstream_branch:
4806 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4807 upstream_commit = upstream_commit.strip()
4808
4809 if not upstream_commit:
4810 DieWithError('Could not find base commit for this branch. '
4811 'Are you in detached state?')
4812
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004813 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4814 diff_output = RunGit(changed_files_cmd)
4815 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004816 # Filter out files deleted by this CL
4817 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004818
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004819 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4820 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4821 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004822 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004823
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004824 top_dir = os.path.normpath(
4825 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
4826
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004827 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4828 # formatted. This is used to block during the presubmit.
4829 return_value = 0
4830
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004831 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004832 # Locate the clang-format binary in the checkout
4833 try:
4834 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07004835 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004836 DieWithError(e)
4837
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004838 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004839 cmd = [clang_format_tool]
4840 if not opts.dry_run and not opts.diff:
4841 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004842 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004843 if opts.diff:
4844 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004845 else:
4846 env = os.environ.copy()
4847 env['PATH'] = str(os.path.dirname(clang_format_tool))
4848 try:
4849 script = clang_format.FindClangFormatScriptInChromiumTree(
4850 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07004851 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004852 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004853
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004854 cmd = [sys.executable, script, '-p0']
4855 if not opts.dry_run and not opts.diff:
4856 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004857
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004858 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
4859 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004860
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004861 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
4862 if opts.diff:
4863 sys.stdout.write(stdout)
4864 if opts.dry_run and len(stdout) > 0:
4865 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004866
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004867 # Similar code to above, but using yapf on .py files rather than clang-format
4868 # on C/C++ files
4869 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004870 yapf_tool = gclient_utils.FindExecutable('yapf')
4871 if yapf_tool is None:
4872 DieWithError('yapf not found in PATH')
4873
4874 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004875 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004876 cmd = [yapf_tool]
4877 if not opts.dry_run and not opts.diff:
4878 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004879 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004880 if opts.diff:
4881 sys.stdout.write(stdout)
4882 else:
4883 # TODO(sbc): yapf --lines mode still has some issues.
4884 # https://github.com/google/yapf/issues/154
4885 DieWithError('--python currently only works with --full')
4886
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004887 # Dart's formatter does not have the nice property of only operating on
4888 # modified chunks, so hard code full.
4889 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004890 try:
4891 command = [dart_format.FindDartFmtToolInChromiumTree()]
4892 if not opts.dry_run and not opts.diff:
4893 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004894 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004895
ppi@chromium.org6593d932016-03-03 15:41:15 +00004896 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004897 if opts.dry_run and stdout:
4898 return_value = 2
4899 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07004900 print('Warning: Unable to check Dart code formatting. Dart SDK not '
4901 'found in this checkout. Files in other languages are still '
4902 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004903
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004904 # Format GN build files. Always run on full build files for canonical form.
4905 if gn_diff_files:
4906 cmd = ['gn', 'format']
4907 if not opts.dry_run and not opts.diff:
4908 cmd.append('--in-place')
4909 for gn_diff_file in gn_diff_files:
bsep@chromium.org627d9002016-04-29 00:00:52 +00004910 stdout = RunCommand(cmd + [gn_diff_file],
4911 shell=sys.platform == 'win32',
4912 cwd=top_dir)
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004913 if opts.diff:
4914 sys.stdout.write(stdout)
4915
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004916 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004917
4918
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004919@subcommand.usage('<codereview url or issue id>')
4920def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004921 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004922 _, args = parser.parse_args(args)
4923
4924 if len(args) != 1:
4925 parser.print_help()
4926 return 1
4927
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004928 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00004929 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004930 parser.print_help()
4931 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00004932 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004933
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004934 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00004935 output = RunGit(['config', '--local', '--get-regexp',
4936 r'branch\..*\.%s' % issueprefix],
4937 error_ok=True)
4938 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004939 if issue == target_issue:
4940 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004941
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004942 branches = []
4943 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00004944 branches.extend(find_issues(cls.IssueSettingSuffix()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004945 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07004946 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004947 return 1
4948 if len(branches) == 1:
4949 RunGit(['checkout', branches[0]])
4950 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004951 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004952 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07004953 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004954 which = raw_input('Choose by index: ')
4955 try:
4956 RunGit(['checkout', branches[int(which)]])
4957 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07004958 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004959 return 1
4960
4961 return 0
4962
4963
maruel@chromium.org29404b52014-09-08 22:58:00 +00004964def CMDlol(parser, args):
4965 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07004966 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00004967 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
4968 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
4969 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07004970 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00004971 return 0
4972
4973
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004974class OptionParser(optparse.OptionParser):
4975 """Creates the option parse and add --verbose support."""
4976 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004977 optparse.OptionParser.__init__(
4978 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004979 self.add_option(
4980 '-v', '--verbose', action='count', default=0,
4981 help='Use 2 times for more debugging info')
4982
4983 def parse_args(self, args=None, values=None):
4984 options, args = optparse.OptionParser.parse_args(self, args, values)
4985 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
4986 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
4987 return options, args
4988
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004989
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004990def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00004991 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07004992 print('\nYour python version %s is unsupported, please upgrade.\n' %
4993 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00004994 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004995
maruel@chromium.orgddd59412011-11-30 14:20:38 +00004996 # Reload settings.
4997 global settings
4998 settings = Settings()
4999
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005000 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005001 dispatcher = subcommand.CommandDispatcher(__name__)
5002 try:
5003 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005004 except auth.AuthenticationError as e:
5005 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005006 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005007 if e.code != 500:
5008 raise
5009 DieWithError(
5010 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5011 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005012 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005013
5014
5015if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005016 # These affect sys.stdout so do it outside of main() to simplify mocks in
5017 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005018 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005019 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005020 try:
5021 sys.exit(main(sys.argv[1:]))
5022 except KeyboardInterrupt:
5023 sys.stderr.write('interrupted\n')
5024 sys.exit(1)