blob: 2fcde04221611aeac7bd5d8ed7b0f4efee99c7da [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
sheyang@google.com6ebaf782015-05-12 19:17:54 +000016import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000017import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000018import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000019import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import optparse
21import os
22import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000023import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025import textwrap
sheyang@google.com6ebaf782015-05-12 19:17:54 +000026import time
27import traceback
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000028import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000030import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000031import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000032import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000033import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000034
35try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000036 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000037except ImportError:
38 pass
39
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000040from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000041from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000043import auth
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000044import clang_format
tandrii@chromium.org71184c02016-01-13 15:18:44 +000045import commit_queue
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000046import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000047import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000048import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000049import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000050import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000051import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000052import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000053import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000054import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000055import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000056import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000057import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000058import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000059import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000060import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000061import watchlists
62
tandrii7400cf02016-06-21 08:48:07 -070063__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000064
tandrii9d2c7a32016-06-22 03:42:45 -070065COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
iannuccie7f68952016-08-15 17:45:29 -070066DEFAULT_SERVER = 'https://codereview.chromium.org'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000067POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000069GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
rmistry@google.comc68112d2015-03-03 12:48:06 +000070REFS_THAT_ALIAS_TO_OTHER_REFS = {
71 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
72 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
73}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000074
thestig@chromium.org44202a22014-03-11 19:22:18 +000075# Valid extensions for files we want to lint.
76DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
77DEFAULT_LINT_IGNORE_REGEX = r"$^"
78
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000079# Shortcut since it quickly becomes redundant.
80Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000081
maruel@chromium.orgddd59412011-11-30 14:20:38 +000082# Initialized in main()
83settings = None
84
85
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000086def DieWithError(message):
vapiera7fbd5a2016-06-16 09:17:49 -070087 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000088 sys.exit(1)
89
90
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000091def GetNoGitPagerEnv():
92 env = os.environ.copy()
93 # 'cat' is a magical git string that disables pagers on all platforms.
94 env['GIT_PAGER'] = 'cat'
95 return env
96
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000097
bsep@chromium.org627d9002016-04-29 00:00:52 +000098def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000099 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000100 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000101 except subprocess2.CalledProcessError as e:
102 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000103 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000104 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000105 'Command "%s" failed.\n%s' % (
106 ' '.join(args), error_message or e.stdout or ''))
107 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000108
109
110def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000111 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000112 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000113
114
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000115def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000116 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700117 if suppress_stderr:
118 stderr = subprocess2.VOID
119 else:
120 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000121 try:
tandrii5d48c322016-08-18 16:19:37 -0700122 (out, _), code = subprocess2.communicate(['git'] + args,
123 env=GetNoGitPagerEnv(),
124 stdout=subprocess2.PIPE,
125 stderr=stderr)
126 return code, out
127 except subprocess2.CalledProcessError as e:
128 logging.debug('Failed running %s', args)
129 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000130
131
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000132def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000133 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000134 return RunGitWithCode(args, suppress_stderr=True)[1]
135
136
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000137def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000138 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000139 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000140 return (version.startswith(prefix) and
141 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000142
143
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000144def BranchExists(branch):
145 """Return True if specified branch exists."""
146 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
147 suppress_stderr=True)
148 return not code
149
150
maruel@chromium.org90541732011-04-01 17:54:18 +0000151def ask_for_data(prompt):
152 try:
153 return raw_input(prompt)
154 except KeyboardInterrupt:
155 # Hide the exception.
156 sys.exit(1)
157
158
tandrii5d48c322016-08-18 16:19:37 -0700159def _git_branch_config_key(branch, key):
160 """Helper method to return Git config key for a branch."""
161 assert branch, 'branch name is required to set git config for it'
162 return 'branch.%s.%s' % (branch, key)
163
164
165def _git_get_branch_config_value(key, default=None, value_type=str,
166 branch=False):
167 """Returns git config value of given or current branch if any.
168
169 Returns default in all other cases.
170 """
171 assert value_type in (int, str, bool)
172 if branch is False: # Distinguishing default arg value from None.
173 branch = GetCurrentBranch()
174
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000175 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700176 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000177
tandrii5d48c322016-08-18 16:19:37 -0700178 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700179 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700180 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700181 # git config also has --int, but apparently git config suffers from integer
182 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700183 args.append(_git_branch_config_key(branch, key))
184 code, out = RunGitWithCode(args)
185 if code == 0:
186 value = out.strip()
187 if value_type == int:
188 return int(value)
189 if value_type == bool:
190 return bool(value.lower() == 'true')
191 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000192 return default
193
194
tandrii5d48c322016-08-18 16:19:37 -0700195def _git_set_branch_config_value(key, value, branch=None, **kwargs):
196 """Sets the value or unsets if it's None of a git branch config.
197
198 Valid, though not necessarily existing, branch must be provided,
199 otherwise currently checked out branch is used.
200 """
201 if not branch:
202 branch = GetCurrentBranch()
203 assert branch, 'a branch name OR currently checked out branch is required'
204 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700205 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700206 if value is None:
207 args.append('--unset')
208 elif isinstance(value, bool):
209 args.append('--bool')
210 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700211 else:
tandrii33a46ff2016-08-23 05:53:40 -0700212 # git config also has --int, but apparently git config suffers from integer
213 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700214 value = str(value)
215 args.append(_git_branch_config_key(branch, key))
216 if value is not None:
217 args.append(value)
218 RunGit(args, **kwargs)
219
220
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000221def add_git_similarity(parser):
222 parser.add_option(
tandrii5d48c322016-08-18 16:19:37 -0700223 '--similarity', metavar='SIM', type=int, action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000224 help='Sets the percentage that a pair of files need to match in order to'
225 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000226 parser.add_option(
227 '--find-copies', action='store_true',
228 help='Allows git to look for copies.')
229 parser.add_option(
230 '--no-find-copies', action='store_false', dest='find_copies',
231 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000232
233 old_parser_args = parser.parse_args
234 def Parse(args):
235 options, args = old_parser_args(args)
236
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000237 if options.similarity is None:
tandrii5d48c322016-08-18 16:19:37 -0700238 options.similarity = _git_get_branch_config_value(
239 'git-cl-similarity', default=50, value_type=int)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000240 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000241 print('Note: Saving similarity of %d%% in git config.'
242 % options.similarity)
tandrii5d48c322016-08-18 16:19:37 -0700243 _git_set_branch_config_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000244
iannucci@chromium.org79540052012-10-19 23:15:26 +0000245 options.similarity = max(0, min(options.similarity, 100))
246
247 if options.find_copies is None:
tandrii5d48c322016-08-18 16:19:37 -0700248 options.find_copies = _git_get_branch_config_value(
249 'git-find-copies', default=True, value_type=bool)
iannucci@chromium.org79540052012-10-19 23:15:26 +0000250 else:
tandrii5d48c322016-08-18 16:19:37 -0700251 _git_set_branch_config_value('git-find-copies', bool(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000252
253 print('Using %d%% similarity for rename/copy detection. '
254 'Override with --similarity.' % options.similarity)
255
256 return options, args
257 parser.parse_args = Parse
258
259
machenbach@chromium.org45453142015-09-15 08:45:22 +0000260def _get_properties_from_options(options):
261 properties = dict(x.split('=', 1) for x in options.properties)
262 for key, val in properties.iteritems():
263 try:
264 properties[key] = json.loads(val)
265 except ValueError:
266 pass # If a value couldn't be evaluated, treat it as a string.
267 return properties
268
269
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000270def _prefix_master(master):
271 """Convert user-specified master name to full master name.
272
273 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
274 name, while the developers always use shortened master name
275 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
276 function does the conversion for buildbucket migration.
277 """
278 prefix = 'master.'
279 if master.startswith(prefix):
280 return master
281 return '%s%s' % (prefix, master)
282
283
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000284def _buildbucket_retry(operation_name, http, *args, **kwargs):
285 """Retries requests to buildbucket service and returns parsed json content."""
286 try_count = 0
287 while True:
288 response, content = http.request(*args, **kwargs)
289 try:
290 content_json = json.loads(content)
291 except ValueError:
292 content_json = None
293
294 # Buildbucket could return an error even if status==200.
295 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000296 error = content_json.get('error')
297 if error.get('code') == 403:
298 raise BuildbucketResponseException(
299 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000300 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000301 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000302 raise BuildbucketResponseException(msg)
303
304 if response.status == 200:
305 if not content_json:
306 raise BuildbucketResponseException(
307 'Buildbucket returns invalid json content: %s.\n'
308 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
309 content)
310 return content_json
311 if response.status < 500 or try_count >= 2:
312 raise httplib2.HttpLib2Error(content)
313
314 # status >= 500 means transient failures.
315 logging.debug('Transient errors when %s. Will retry.', operation_name)
316 time.sleep(0.5 + 1.5*try_count)
317 try_count += 1
318 assert False, 'unreachable'
319
320
machenbach@chromium.org45453142015-09-15 08:45:22 +0000321def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000322 rietveld_url = settings.GetDefaultServerUrl()
323 rietveld_host = urlparse.urlparse(rietveld_url).hostname
324 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
325 http = authenticator.authorize(httplib2.Http())
326 http.force_exception_to_status_code = True
327 issue_props = changelist.GetIssueProperties()
328 issue = changelist.GetIssue()
329 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000330 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000331
332 buildbucket_put_url = (
333 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000334 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000335 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
336 hostname=rietveld_host,
337 issue=issue,
338 patch=patchset)
339
340 batch_req_body = {'builds': []}
341 print_text = []
342 print_text.append('Tried jobs on:')
343 for master, builders_and_tests in sorted(masters.iteritems()):
344 print_text.append('Master: %s' % master)
345 bucket = _prefix_master(master)
346 for builder, tests in sorted(builders_and_tests.iteritems()):
347 print_text.append(' %s: %s' % (builder, tests))
348 parameters = {
349 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000350 'changes': [{
351 'author': {'email': issue_props['owner_email']},
352 'revision': options.revision,
353 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000354 'properties': {
355 'category': category,
356 'issue': issue,
357 'master': master,
358 'patch_project': issue_props['project'],
359 'patch_storage': 'rietveld',
360 'patchset': patchset,
361 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000362 'rietveld': rietveld_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000363 },
364 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000365 if 'presubmit' in builder.lower():
366 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000367 if tests:
368 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000369 if properties:
370 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000371 if options.clobber:
372 parameters['properties']['clobber'] = True
373 batch_req_body['builds'].append(
374 {
375 'bucket': bucket,
376 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000377 'client_operation_id': str(uuid.uuid4()),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000378 'tags': ['builder:%s' % builder,
379 'buildset:%s' % buildset,
380 'master:%s' % master,
381 'user_agent:git_cl_try']
382 }
383 )
384
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000385 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700386 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000387 http,
388 buildbucket_put_url,
389 'PUT',
390 body=json.dumps(batch_req_body),
391 headers={'Content-Type': 'application/json'}
392 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000393 print_text.append('To see results here, run: git cl try-results')
394 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700395 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000396
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000397
tandrii221ab252016-10-06 08:12:04 -0700398def fetch_try_jobs(auth_config, changelist, buildbucket_host,
399 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700400 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000401
qyearsley53f48a12016-09-01 10:45:13 -0700402 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000403 """
tandrii221ab252016-10-06 08:12:04 -0700404 assert buildbucket_host
405 assert changelist.GetIssue(), 'CL must be uploaded first'
406 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
407 patchset = patchset or changelist.GetMostRecentPatchset()
408 assert patchset, 'CL must be uploaded first'
409
410 codereview_url = changelist.GetCodereviewServer()
411 codereview_host = urlparse.urlparse(codereview_url).hostname
412 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000413 if authenticator.has_cached_credentials():
414 http = authenticator.authorize(httplib2.Http())
415 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700416 print('Warning: Some results might be missing because %s' %
417 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700418 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000419 http = httplib2.Http()
420
421 http.force_exception_to_status_code = True
422
tandrii221ab252016-10-06 08:12:04 -0700423 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
424 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
425 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000426 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700427 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000428 params = {'tag': 'buildset:%s' % buildset}
429
430 builds = {}
431 while True:
432 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700433 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000434 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700435 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000436 for build in content.get('builds', []):
437 builds[build['id']] = build
438 if 'next_cursor' in content:
439 params['start_cursor'] = content['next_cursor']
440 else:
441 break
442 return builds
443
444
qyearsleyeab3c042016-08-24 09:18:28 -0700445def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000446 """Prints nicely result of fetch_try_jobs."""
447 if not builds:
qyearsleyeab3c042016-08-24 09:18:28 -0700448 print('No try jobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000449 return
450
451 # Make a copy, because we'll be modifying builds dictionary.
452 builds = builds.copy()
453 builder_names_cache = {}
454
455 def get_builder(b):
456 try:
457 return builder_names_cache[b['id']]
458 except KeyError:
459 try:
460 parameters = json.loads(b['parameters_json'])
461 name = parameters['builder_name']
462 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700463 print('WARNING: failed to get builder name for build %s: %s' % (
464 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000465 name = None
466 builder_names_cache[b['id']] = name
467 return name
468
469 def get_bucket(b):
470 bucket = b['bucket']
471 if bucket.startswith('master.'):
472 return bucket[len('master.'):]
473 return bucket
474
475 if options.print_master:
476 name_fmt = '%%-%ds %%-%ds' % (
477 max(len(str(get_bucket(b))) for b in builds.itervalues()),
478 max(len(str(get_builder(b))) for b in builds.itervalues()))
479 def get_name(b):
480 return name_fmt % (get_bucket(b), get_builder(b))
481 else:
482 name_fmt = '%%-%ds' % (
483 max(len(str(get_builder(b))) for b in builds.itervalues()))
484 def get_name(b):
485 return name_fmt % get_builder(b)
486
487 def sort_key(b):
488 return b['status'], b.get('result'), get_name(b), b.get('url')
489
490 def pop(title, f, color=None, **kwargs):
491 """Pop matching builds from `builds` dict and print them."""
492
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000493 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000494 colorize = str
495 else:
496 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
497
498 result = []
499 for b in builds.values():
500 if all(b.get(k) == v for k, v in kwargs.iteritems()):
501 builds.pop(b['id'])
502 result.append(b)
503 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700504 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000505 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700506 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000507
508 total = len(builds)
509 pop(status='COMPLETED', result='SUCCESS',
510 title='Successes:', color=Fore.GREEN,
511 f=lambda b: (get_name(b), b.get('url')))
512 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
513 title='Infra Failures:', color=Fore.MAGENTA,
514 f=lambda b: (get_name(b), b.get('url')))
515 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
516 title='Failures:', color=Fore.RED,
517 f=lambda b: (get_name(b), b.get('url')))
518 pop(status='COMPLETED', result='CANCELED',
519 title='Canceled:', color=Fore.MAGENTA,
520 f=lambda b: (get_name(b),))
521 pop(status='COMPLETED', result='FAILURE',
522 failure_reason='INVALID_BUILD_DEFINITION',
523 title='Wrong master/builder name:', color=Fore.MAGENTA,
524 f=lambda b: (get_name(b),))
525 pop(status='COMPLETED', result='FAILURE',
526 title='Other failures:',
527 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
528 pop(status='COMPLETED',
529 title='Other finished:',
530 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
531 pop(status='STARTED',
532 title='Started:', color=Fore.YELLOW,
533 f=lambda b: (get_name(b), b.get('url')))
534 pop(status='SCHEDULED',
535 title='Scheduled:',
536 f=lambda b: (get_name(b), 'id=%s' % b['id']))
537 # The last section is just in case buildbucket API changes OR there is a bug.
538 pop(title='Other:',
539 f=lambda b: (get_name(b), 'id=%s' % b['id']))
540 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700541 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000542
543
qyearsley53f48a12016-09-01 10:45:13 -0700544def write_try_results_json(output_file, builds):
545 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
546
547 The input |builds| dict is assumed to be generated by Buildbucket.
548 Buildbucket documentation: http://goo.gl/G0s101
549 """
550
551 def convert_build_dict(build):
552 return {
553 'buildbucket_id': build.get('id'),
554 'status': build.get('status'),
555 'result': build.get('result'),
556 'bucket': build.get('bucket'),
557 'builder_name': json.loads(
558 build.get('parameters_json', '{}')).get('builder_name'),
559 'failure_reason': build.get('failure_reason'),
560 'url': build.get('url'),
561 }
562
563 converted = []
564 for _, build in sorted(builds.items()):
565 converted.append(convert_build_dict(build))
566 write_json(output_file, converted)
567
568
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000569def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
570 """Return the corresponding git ref if |base_url| together with |glob_spec|
571 matches the full |url|.
572
573 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
574 """
575 fetch_suburl, as_ref = glob_spec.split(':')
576 if allow_wildcards:
577 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
578 if glob_match:
579 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
580 # "branches/{472,597,648}/src:refs/remotes/svn/*".
581 branch_re = re.escape(base_url)
582 if glob_match.group(1):
583 branch_re += '/' + re.escape(glob_match.group(1))
584 wildcard = glob_match.group(2)
585 if wildcard == '*':
586 branch_re += '([^/]*)'
587 else:
588 # Escape and replace surrounding braces with parentheses and commas
589 # with pipe symbols.
590 wildcard = re.escape(wildcard)
591 wildcard = re.sub('^\\\\{', '(', wildcard)
592 wildcard = re.sub('\\\\,', '|', wildcard)
593 wildcard = re.sub('\\\\}$', ')', wildcard)
594 branch_re += wildcard
595 if glob_match.group(3):
596 branch_re += re.escape(glob_match.group(3))
597 match = re.match(branch_re, url)
598 if match:
599 return re.sub('\*$', match.group(1), as_ref)
600
601 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
602 if fetch_suburl:
603 full_url = base_url + '/' + fetch_suburl
604 else:
605 full_url = base_url
606 if full_url == url:
607 return as_ref
608 return None
609
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000610
iannucci@chromium.org79540052012-10-19 23:15:26 +0000611def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000612 """Prints statistics about the change to the user."""
613 # --no-ext-diff is broken in some versions of Git, so try to work around
614 # this by overriding the environment (but there is still a problem if the
615 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000616 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000617 if 'GIT_EXTERNAL_DIFF' in env:
618 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000619
620 if find_copies:
621 similarity_options = ['--find-copies-harder', '-l100000',
622 '-C%s' % similarity]
623 else:
624 similarity_options = ['-M%s' % similarity]
625
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000626 try:
627 stdout = sys.stdout.fileno()
628 except AttributeError:
629 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000630 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000631 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000632 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000633 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000634
635
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000636class BuildbucketResponseException(Exception):
637 pass
638
639
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000640class Settings(object):
641 def __init__(self):
642 self.default_server = None
643 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000644 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000645 self.is_git_svn = None
646 self.svn_branch = None
647 self.tree_status_url = None
648 self.viewvc_url = None
649 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000650 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000651 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000652 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000653 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000654 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000655 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000656 self.pending_ref_prefix = None
tandriif46c20f2016-09-14 06:17:05 -0700657 self.git_number_footer = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000658
659 def LazyUpdateIfNeeded(self):
660 """Updates the settings from a codereview.settings file, if available."""
661 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000662 # The only value that actually changes the behavior is
663 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000664 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000665 error_ok=True
666 ).strip().lower()
667
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000668 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000669 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000670 LoadCodereviewSettingsFromFile(cr_settings_file)
671 self.updated = True
672
673 def GetDefaultServerUrl(self, error_ok=False):
674 if not self.default_server:
675 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000676 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000677 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000678 if error_ok:
679 return self.default_server
680 if not self.default_server:
681 error_message = ('Could not find settings file. You must configure '
682 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000683 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000684 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000685 return self.default_server
686
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000687 @staticmethod
688 def GetRelativeRoot():
689 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000690
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000691 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000692 if self.root is None:
693 self.root = os.path.abspath(self.GetRelativeRoot())
694 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000695
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000696 def GetGitMirror(self, remote='origin'):
697 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000698 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000699 if not os.path.isdir(local_url):
700 return None
701 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
702 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
703 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
704 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
705 if mirror.exists():
706 return mirror
707 return None
708
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000709 def GetIsGitSvn(self):
710 """Return true if this repo looks like it's using git-svn."""
711 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000712 if self.GetPendingRefPrefix():
713 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
714 self.is_git_svn = False
715 else:
716 # If you have any "svn-remote.*" config keys, we think you're using svn.
717 self.is_git_svn = RunGitWithCode(
718 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000719 return self.is_git_svn
720
721 def GetSVNBranch(self):
722 if self.svn_branch is None:
723 if not self.GetIsGitSvn():
724 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
725
726 # Try to figure out which remote branch we're based on.
727 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000728 # 1) iterate through our branch history and find the svn URL.
729 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000730
731 # regexp matching the git-svn line that contains the URL.
732 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
733
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000734 # We don't want to go through all of history, so read a line from the
735 # pipe at a time.
736 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000737 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000738 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
739 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000740 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000741 for line in proc.stdout:
742 match = git_svn_re.match(line)
743 if match:
744 url = match.group(1)
745 proc.stdout.close() # Cut pipe.
746 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000747
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000748 if url:
749 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
750 remotes = RunGit(['config', '--get-regexp',
751 r'^svn-remote\..*\.url']).splitlines()
752 for remote in remotes:
753 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000754 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000755 remote = match.group(1)
756 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000757 rewrite_root = RunGit(
758 ['config', 'svn-remote.%s.rewriteRoot' % remote],
759 error_ok=True).strip()
760 if rewrite_root:
761 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000762 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000763 ['config', 'svn-remote.%s.fetch' % remote],
764 error_ok=True).strip()
765 if fetch_spec:
766 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
767 if self.svn_branch:
768 break
769 branch_spec = RunGit(
770 ['config', 'svn-remote.%s.branches' % remote],
771 error_ok=True).strip()
772 if branch_spec:
773 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
774 if self.svn_branch:
775 break
776 tag_spec = RunGit(
777 ['config', 'svn-remote.%s.tags' % remote],
778 error_ok=True).strip()
779 if tag_spec:
780 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
781 if self.svn_branch:
782 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000783
784 if not self.svn_branch:
785 DieWithError('Can\'t guess svn branch -- try specifying it on the '
786 'command line')
787
788 return self.svn_branch
789
790 def GetTreeStatusUrl(self, error_ok=False):
791 if not self.tree_status_url:
792 error_message = ('You must configure your tree status URL by running '
793 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000794 self.tree_status_url = self._GetRietveldConfig(
795 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000796 return self.tree_status_url
797
798 def GetViewVCUrl(self):
799 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000800 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000801 return self.viewvc_url
802
rmistry@google.com90752582014-01-14 21:04:50 +0000803 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000804 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000805
rmistry@google.com78948ed2015-07-08 23:09:57 +0000806 def GetIsSkipDependencyUpload(self, branch_name):
807 """Returns true if specified branch should skip dep uploads."""
808 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
809 error_ok=True)
810
rmistry@google.com5626a922015-02-26 14:03:30 +0000811 def GetRunPostUploadHook(self):
812 run_post_upload_hook = self._GetRietveldConfig(
813 'run-post-upload-hook', error_ok=True)
814 return run_post_upload_hook == "True"
815
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000816 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000817 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000818
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000819 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000820 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000821
ukai@chromium.orge8077812012-02-03 03:41:46 +0000822 def GetIsGerrit(self):
823 """Return true if this repo is assosiated with gerrit code review system."""
824 if self.is_gerrit is None:
825 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
826 return self.is_gerrit
827
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000828 def GetSquashGerritUploads(self):
829 """Return true if uploads to Gerrit should be squashed by default."""
830 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700831 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
832 if self.squash_gerrit_uploads is None:
833 # Default is squash now (http://crbug.com/611892#c23).
834 self.squash_gerrit_uploads = not (
835 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
836 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000837 return self.squash_gerrit_uploads
838
tandriia60502f2016-06-20 02:01:53 -0700839 def GetSquashGerritUploadsOverride(self):
840 """Return True or False if codereview.settings should be overridden.
841
842 Returns None if no override has been defined.
843 """
844 # See also http://crbug.com/611892#c23
845 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
846 error_ok=True).strip()
847 if result == 'true':
848 return True
849 if result == 'false':
850 return False
851 return None
852
tandrii@chromium.org28253532016-04-14 13:46:56 +0000853 def GetGerritSkipEnsureAuthenticated(self):
854 """Return True if EnsureAuthenticated should not be done for Gerrit
855 uploads."""
856 if self.gerrit_skip_ensure_authenticated is None:
857 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000858 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000859 error_ok=True).strip() == 'true')
860 return self.gerrit_skip_ensure_authenticated
861
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000862 def GetGitEditor(self):
863 """Return the editor specified in the git config, or None if none is."""
864 if self.git_editor is None:
865 self.git_editor = self._GetConfig('core.editor', error_ok=True)
866 return self.git_editor or None
867
thestig@chromium.org44202a22014-03-11 19:22:18 +0000868 def GetLintRegex(self):
869 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
870 DEFAULT_LINT_REGEX)
871
872 def GetLintIgnoreRegex(self):
873 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
874 DEFAULT_LINT_IGNORE_REGEX)
875
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000876 def GetProject(self):
877 if not self.project:
878 self.project = self._GetRietveldConfig('project', error_ok=True)
879 return self.project
880
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000881 def GetForceHttpsCommitUrl(self):
882 if not self.force_https_commit_url:
883 self.force_https_commit_url = self._GetRietveldConfig(
884 'force-https-commit-url', error_ok=True)
885 return self.force_https_commit_url
886
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000887 def GetPendingRefPrefix(self):
888 if not self.pending_ref_prefix:
889 self.pending_ref_prefix = self._GetRietveldConfig(
890 'pending-ref-prefix', error_ok=True)
891 return self.pending_ref_prefix
892
tandriif46c20f2016-09-14 06:17:05 -0700893 def GetHasGitNumberFooter(self):
894 # TODO(tandrii): this has to be removed after Rietveld is read-only.
895 # see also bugs http://crbug.com/642493 and http://crbug.com/600469.
896 if not self.git_number_footer:
897 self.git_number_footer = self._GetRietveldConfig(
898 'git-number-footer', error_ok=True)
899 return self.git_number_footer
900
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000901 def _GetRietveldConfig(self, param, **kwargs):
902 return self._GetConfig('rietveld.' + param, **kwargs)
903
rmistry@google.com78948ed2015-07-08 23:09:57 +0000904 def _GetBranchConfig(self, branch_name, param, **kwargs):
905 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
906
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000907 def _GetConfig(self, param, **kwargs):
908 self.LazyUpdateIfNeeded()
909 return RunGit(['config', param], **kwargs).strip()
910
911
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000912def ShortBranchName(branch):
913 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000914 return branch.replace('refs/heads/', '', 1)
915
916
917def GetCurrentBranchRef():
918 """Returns branch ref (e.g., refs/heads/master) or None."""
919 return RunGit(['symbolic-ref', 'HEAD'],
920 stderr=subprocess2.VOID, error_ok=True).strip() or None
921
922
923def GetCurrentBranch():
924 """Returns current branch or None.
925
926 For refs/heads/* branches, returns just last part. For others, full ref.
927 """
928 branchref = GetCurrentBranchRef()
929 if branchref:
930 return ShortBranchName(branchref)
931 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000932
933
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000934class _CQState(object):
935 """Enum for states of CL with respect to Commit Queue."""
936 NONE = 'none'
937 DRY_RUN = 'dry_run'
938 COMMIT = 'commit'
939
940 ALL_STATES = [NONE, DRY_RUN, COMMIT]
941
942
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000943class _ParsedIssueNumberArgument(object):
944 def __init__(self, issue=None, patchset=None, hostname=None):
945 self.issue = issue
946 self.patchset = patchset
947 self.hostname = hostname
948
949 @property
950 def valid(self):
951 return self.issue is not None
952
953
954class _RietveldParsedIssueNumberArgument(_ParsedIssueNumberArgument):
955 def __init__(self, *args, **kwargs):
956 self.patch_url = kwargs.pop('patch_url', None)
957 super(_RietveldParsedIssueNumberArgument, self).__init__(*args, **kwargs)
958
959
960def ParseIssueNumberArgument(arg):
961 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
962 fail_result = _ParsedIssueNumberArgument()
963
964 if arg.isdigit():
965 return _ParsedIssueNumberArgument(issue=int(arg))
966 if not arg.startswith('http'):
967 return fail_result
968 url = gclient_utils.UpgradeToHttps(arg)
969 try:
970 parsed_url = urlparse.urlparse(url)
971 except ValueError:
972 return fail_result
973 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
974 tmp = cls.ParseIssueURL(parsed_url)
975 if tmp is not None:
976 return tmp
977 return fail_result
978
979
tandriic2405f52016-10-10 08:13:15 -0700980class GerritIssueNotExists(Exception):
981 def __init__(self, issue, url):
982 self.issue = issue
983 self.url = url
984 super(GerritIssueNotExists, self).__init__()
985
986 def __str__(self):
987 return 'issue %s at %s does not exist or you have no access to it' % (
988 self.issue, self.url)
989
990
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000991class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000992 """Changelist works with one changelist in local branch.
993
994 Supports two codereview backends: Rietveld or Gerrit, selected at object
995 creation.
996
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +0000997 Notes:
998 * Not safe for concurrent multi-{thread,process} use.
999 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001000 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001001 """
1002
1003 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1004 """Create a new ChangeList instance.
1005
1006 If issue is given, the codereview must be given too.
1007
1008 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1009 Otherwise, it's decided based on current configuration of the local branch,
1010 with default being 'rietveld' for backwards compatibility.
1011 See _load_codereview_impl for more details.
1012
1013 **kwargs will be passed directly to codereview implementation.
1014 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001015 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001016 global settings
1017 if not settings:
1018 # Happens when git_cl.py is used as a utility library.
1019 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001020
1021 if issue:
1022 assert codereview, 'codereview must be known, if issue is known'
1023
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001024 self.branchref = branchref
1025 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001026 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001027 self.branch = ShortBranchName(self.branchref)
1028 else:
1029 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001030 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001031 self.lookedup_issue = False
1032 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001033 self.has_description = False
1034 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001035 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001036 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001037 self.cc = None
1038 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001039 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001040
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001041 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001042 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001043 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001044 assert self._codereview_impl
1045 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001046
1047 def _load_codereview_impl(self, codereview=None, **kwargs):
1048 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001049 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1050 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1051 self._codereview = codereview
1052 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001053 return
1054
1055 # Automatic selection based on issue number set for a current branch.
1056 # Rietveld takes precedence over Gerrit.
1057 assert not self.issue
1058 # Whether we find issue or not, we are doing the lookup.
1059 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001060 if self.GetBranch():
1061 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1062 issue = _git_get_branch_config_value(
1063 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1064 if issue:
1065 self._codereview = codereview
1066 self._codereview_impl = cls(self, **kwargs)
1067 self.issue = int(issue)
1068 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001069
1070 # No issue is set for this branch, so decide based on repo-wide settings.
1071 return self._load_codereview_impl(
1072 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1073 **kwargs)
1074
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001075 def IsGerrit(self):
1076 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001077
1078 def GetCCList(self):
1079 """Return the users cc'd on this CL.
1080
agable92bec4f2016-08-24 09:27:27 -07001081 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001082 """
1083 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001084 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001085 more_cc = ','.join(self.watchers)
1086 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1087 return self.cc
1088
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001089 def GetCCListWithoutDefault(self):
1090 """Return the users cc'd on this CL excluding default ones."""
1091 if self.cc is None:
1092 self.cc = ','.join(self.watchers)
1093 return self.cc
1094
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001095 def SetWatchers(self, watchers):
1096 """Set the list of email addresses that should be cc'd based on the changed
1097 files in this CL.
1098 """
1099 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001100
1101 def GetBranch(self):
1102 """Returns the short branch name, e.g. 'master'."""
1103 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001104 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001105 if not branchref:
1106 return None
1107 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001108 self.branch = ShortBranchName(self.branchref)
1109 return self.branch
1110
1111 def GetBranchRef(self):
1112 """Returns the full branch name, e.g. 'refs/heads/master'."""
1113 self.GetBranch() # Poke the lazy loader.
1114 return self.branchref
1115
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001116 def ClearBranch(self):
1117 """Clears cached branch data of this object."""
1118 self.branch = self.branchref = None
1119
tandrii5d48c322016-08-18 16:19:37 -07001120 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1121 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1122 kwargs['branch'] = self.GetBranch()
1123 return _git_get_branch_config_value(key, default, **kwargs)
1124
1125 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1126 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1127 assert self.GetBranch(), (
1128 'this CL must have an associated branch to %sset %s%s' %
1129 ('un' if value is None else '',
1130 key,
1131 '' if value is None else ' to %r' % value))
1132 kwargs['branch'] = self.GetBranch()
1133 return _git_set_branch_config_value(key, value, **kwargs)
1134
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001135 @staticmethod
1136 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001137 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001138 e.g. 'origin', 'refs/heads/master'
1139 """
1140 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001141 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1142
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001143 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001144 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001145 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001146 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1147 error_ok=True).strip()
1148 if upstream_branch:
1149 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001150 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001151 # Fall back on trying a git-svn upstream branch.
1152 if settings.GetIsGitSvn():
1153 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001154 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001155 # Else, try to guess the origin remote.
1156 remote_branches = RunGit(['branch', '-r']).split()
1157 if 'origin/master' in remote_branches:
1158 # Fall back on origin/master if it exits.
1159 remote = 'origin'
1160 upstream_branch = 'refs/heads/master'
1161 elif 'origin/trunk' in remote_branches:
1162 # Fall back on origin/trunk if it exists. Generally a shared
1163 # git-svn clone
1164 remote = 'origin'
1165 upstream_branch = 'refs/heads/trunk'
1166 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001167 DieWithError(
1168 'Unable to determine default branch to diff against.\n'
1169 'Either pass complete "git diff"-style arguments, like\n'
1170 ' git cl upload origin/master\n'
1171 'or verify this branch is set up to track another \n'
1172 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001173
1174 return remote, upstream_branch
1175
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001176 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001177 upstream_branch = self.GetUpstreamBranch()
1178 if not BranchExists(upstream_branch):
1179 DieWithError('The upstream for the current branch (%s) does not exist '
1180 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001181 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001182 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001183
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001184 def GetUpstreamBranch(self):
1185 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001186 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001187 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001188 upstream_branch = upstream_branch.replace('refs/heads/',
1189 'refs/remotes/%s/' % remote)
1190 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1191 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001192 self.upstream_branch = upstream_branch
1193 return self.upstream_branch
1194
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001195 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001196 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001197 remote, branch = None, self.GetBranch()
1198 seen_branches = set()
1199 while branch not in seen_branches:
1200 seen_branches.add(branch)
1201 remote, branch = self.FetchUpstreamTuple(branch)
1202 branch = ShortBranchName(branch)
1203 if remote != '.' or branch.startswith('refs/remotes'):
1204 break
1205 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001206 remotes = RunGit(['remote'], error_ok=True).split()
1207 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001208 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001209 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001210 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001211 logging.warning('Could not determine which remote this change is '
1212 'associated with, so defaulting to "%s". This may '
1213 'not be what you want. You may prevent this message '
1214 'by running "git svn info" as documented here: %s',
1215 self._remote,
1216 GIT_INSTRUCTIONS_URL)
1217 else:
1218 logging.warn('Could not determine which remote this change is '
1219 'associated with. You may prevent this message by '
1220 'running "git svn info" as documented here: %s',
1221 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001222 branch = 'HEAD'
1223 if branch.startswith('refs/remotes'):
1224 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001225 elif branch.startswith('refs/branch-heads/'):
1226 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001227 else:
1228 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001229 return self._remote
1230
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001231 def GitSanityChecks(self, upstream_git_obj):
1232 """Checks git repo status and ensures diff is from local commits."""
1233
sbc@chromium.org79706062015-01-14 21:18:12 +00001234 if upstream_git_obj is None:
1235 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001236 print('ERROR: unable to determine current branch (detached HEAD?)',
1237 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001238 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001239 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001240 return False
1241
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001242 # Verify the commit we're diffing against is in our current branch.
1243 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1244 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1245 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001246 print('ERROR: %s is not in the current branch. You may need to rebase '
1247 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001248 return False
1249
1250 # List the commits inside the diff, and verify they are all local.
1251 commits_in_diff = RunGit(
1252 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1253 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1254 remote_branch = remote_branch.strip()
1255 if code != 0:
1256 _, remote_branch = self.GetRemoteBranch()
1257
1258 commits_in_remote = RunGit(
1259 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1260
1261 common_commits = set(commits_in_diff) & set(commits_in_remote)
1262 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001263 print('ERROR: Your diff contains %d commits already in %s.\n'
1264 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1265 'the diff. If you are using a custom git flow, you can override'
1266 ' the reference used for this check with "git config '
1267 'gitcl.remotebranch <git-ref>".' % (
1268 len(common_commits), remote_branch, upstream_git_obj),
1269 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001270 return False
1271 return True
1272
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001273 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001274 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001275
1276 Returns None if it is not set.
1277 """
tandrii5d48c322016-08-18 16:19:37 -07001278 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001279
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001280 def GetGitSvnRemoteUrl(self):
1281 """Return the configured git-svn remote URL parsed from git svn info.
1282
1283 Returns None if it is not set.
1284 """
1285 # URL is dependent on the current directory.
1286 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1287 if data:
1288 keys = dict(line.split(': ', 1) for line in data.splitlines()
1289 if ': ' in line)
1290 return keys.get('URL', None)
1291 return None
1292
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001293 def GetRemoteUrl(self):
1294 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1295
1296 Returns None if there is no remote.
1297 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001298 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001299 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1300
1301 # If URL is pointing to a local directory, it is probably a git cache.
1302 if os.path.isdir(url):
1303 url = RunGit(['config', 'remote.%s.url' % remote],
1304 error_ok=True,
1305 cwd=url).strip()
1306 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001307
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001308 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001309 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001310 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001311 self.issue = self._GitGetBranchConfigValue(
1312 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001313 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001314 return self.issue
1315
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001316 def GetIssueURL(self):
1317 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001318 issue = self.GetIssue()
1319 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001320 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001321 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001322
1323 def GetDescription(self, pretty=False):
1324 if not self.has_description:
1325 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001326 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001327 self.has_description = True
1328 if pretty:
1329 wrapper = textwrap.TextWrapper()
1330 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1331 return wrapper.fill(self.description)
1332 return self.description
1333
1334 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001335 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001336 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001337 self.patchset = self._GitGetBranchConfigValue(
1338 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001339 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001340 return self.patchset
1341
1342 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001343 """Set this branch's patchset. If patchset=0, clears the patchset."""
1344 assert self.GetBranch()
1345 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001346 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001347 else:
1348 self.patchset = int(patchset)
1349 self._GitSetBranchConfigValue(
1350 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001351
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001352 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001353 """Set this branch's issue. If issue isn't given, clears the issue."""
1354 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001355 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001356 issue = int(issue)
1357 self._GitSetBranchConfigValue(
1358 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001359 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001360 codereview_server = self._codereview_impl.GetCodereviewServer()
1361 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001362 self._GitSetBranchConfigValue(
1363 self._codereview_impl.CodereviewServerConfigKey(),
1364 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001365 else:
tandrii5d48c322016-08-18 16:19:37 -07001366 # Reset all of these just to be clean.
1367 reset_suffixes = [
1368 'last-upload-hash',
1369 self._codereview_impl.IssueConfigKey(),
1370 self._codereview_impl.PatchsetConfigKey(),
1371 self._codereview_impl.CodereviewServerConfigKey(),
1372 ] + self._PostUnsetIssueProperties()
1373 for prop in reset_suffixes:
1374 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001375 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001376 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001377
dnjba1b0f32016-09-02 12:37:42 -07001378 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001379 if not self.GitSanityChecks(upstream_branch):
1380 DieWithError('\nGit sanity check failure')
1381
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001382 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001383 if not root:
1384 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001385 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001386
1387 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001388 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001389 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001390 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001391 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001392 except subprocess2.CalledProcessError:
1393 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001394 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001395 'This branch probably doesn\'t exist anymore. To reset the\n'
1396 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001397 ' git branch --set-upstream-to origin/master %s\n'
1398 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001399 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001400
maruel@chromium.org52424302012-08-29 15:14:30 +00001401 issue = self.GetIssue()
1402 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001403 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001404 description = self.GetDescription()
1405 else:
1406 # If the change was never uploaded, use the log messages of all commits
1407 # up to the branch point, as git cl upload will prefill the description
1408 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001409 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1410 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001411
1412 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001413 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001414 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001415 name,
1416 description,
1417 absroot,
1418 files,
1419 issue,
1420 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001421 author,
1422 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001423
dsansomee2d6fd92016-09-08 00:10:47 -07001424 def UpdateDescription(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001425 self.description = description
dsansomee2d6fd92016-09-08 00:10:47 -07001426 return self._codereview_impl.UpdateDescriptionRemote(
1427 description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001428
1429 def RunHook(self, committing, may_prompt, verbose, change):
1430 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1431 try:
1432 return presubmit_support.DoPresubmitChecks(change, committing,
1433 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1434 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001435 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1436 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001437 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001438 DieWithError(
1439 ('%s\nMaybe your depot_tools is out of date?\n'
1440 'If all fails, contact maruel@') % e)
1441
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001442 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1443 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001444 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1445 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001446 else:
1447 # Assume url.
1448 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1449 urlparse.urlparse(issue_arg))
1450 if not parsed_issue_arg or not parsed_issue_arg.valid:
1451 DieWithError('Failed to parse issue argument "%s". '
1452 'Must be an issue number or a valid URL.' % issue_arg)
1453 return self._codereview_impl.CMDPatchWithParsedIssue(
1454 parsed_issue_arg, reject, nocommit, directory)
1455
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001456 def CMDUpload(self, options, git_diff_args, orig_args):
1457 """Uploads a change to codereview."""
1458 if git_diff_args:
1459 # TODO(ukai): is it ok for gerrit case?
1460 base_branch = git_diff_args[0]
1461 else:
1462 if self.GetBranch() is None:
1463 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1464
1465 # Default to diffing against common ancestor of upstream branch
1466 base_branch = self.GetCommonAncestorWithUpstream()
1467 git_diff_args = [base_branch, 'HEAD']
1468
1469 # Make sure authenticated to codereview before running potentially expensive
1470 # hooks. It is a fast, best efforts check. Codereview still can reject the
1471 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001472 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001473
1474 # Apply watchlists on upload.
1475 change = self.GetChange(base_branch, None)
1476 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1477 files = [f.LocalPath() for f in change.AffectedFiles()]
1478 if not options.bypass_watchlists:
1479 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1480
1481 if not options.bypass_hooks:
1482 if options.reviewers or options.tbr_owners:
1483 # Set the reviewer list now so that presubmit checks can access it.
1484 change_description = ChangeDescription(change.FullDescriptionText())
1485 change_description.update_reviewers(options.reviewers,
1486 options.tbr_owners,
1487 change)
1488 change.SetDescriptionText(change_description.description)
1489 hook_results = self.RunHook(committing=False,
1490 may_prompt=not options.force,
1491 verbose=options.verbose,
1492 change=change)
1493 if not hook_results.should_continue():
1494 return 1
1495 if not options.reviewers and hook_results.reviewers:
1496 options.reviewers = hook_results.reviewers.split(',')
1497
1498 if self.GetIssue():
1499 latest_patchset = self.GetMostRecentPatchset()
1500 local_patchset = self.GetPatchset()
1501 if (latest_patchset and local_patchset and
1502 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001503 print('The last upload made from this repository was patchset #%d but '
1504 'the most recent patchset on the server is #%d.'
1505 % (local_patchset, latest_patchset))
1506 print('Uploading will still work, but if you\'ve uploaded to this '
1507 'issue from another machine or branch the patch you\'re '
1508 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001509 ask_for_data('About to upload; enter to confirm.')
1510
1511 print_stats(options.similarity, options.find_copies, git_diff_args)
1512 ret = self.CMDUploadChange(options, git_diff_args, change)
1513 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001514 if options.use_commit_queue:
1515 self.SetCQState(_CQState.COMMIT)
1516 elif options.cq_dry_run:
1517 self.SetCQState(_CQState.DRY_RUN)
1518
tandrii5d48c322016-08-18 16:19:37 -07001519 _git_set_branch_config_value('last-upload-hash',
1520 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001521 # Run post upload hooks, if specified.
1522 if settings.GetRunPostUploadHook():
1523 presubmit_support.DoPostUploadExecuter(
1524 change,
1525 self,
1526 settings.GetRoot(),
1527 options.verbose,
1528 sys.stdout)
1529
1530 # Upload all dependencies if specified.
1531 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001532 print()
1533 print('--dependencies has been specified.')
1534 print('All dependent local branches will be re-uploaded.')
1535 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001536 # Remove the dependencies flag from args so that we do not end up in a
1537 # loop.
1538 orig_args.remove('--dependencies')
1539 ret = upload_branch_deps(self, orig_args)
1540 return ret
1541
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001542 def SetCQState(self, new_state):
1543 """Update the CQ state for latest patchset.
1544
1545 Issue must have been already uploaded and known.
1546 """
1547 assert new_state in _CQState.ALL_STATES
1548 assert self.GetIssue()
1549 return self._codereview_impl.SetCQState(new_state)
1550
tandriie113dfd2016-10-11 10:20:12 -07001551 def CannotTriggerTryJobReason(self):
1552 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1553 return self._codereview_impl.CannotTriggerTryJobReason()
1554
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001555 # Forward methods to codereview specific implementation.
1556
1557 def CloseIssue(self):
1558 return self._codereview_impl.CloseIssue()
1559
1560 def GetStatus(self):
1561 return self._codereview_impl.GetStatus()
1562
1563 def GetCodereviewServer(self):
1564 return self._codereview_impl.GetCodereviewServer()
1565
1566 def GetApprovingReviewers(self):
1567 return self._codereview_impl.GetApprovingReviewers()
1568
1569 def GetMostRecentPatchset(self):
1570 return self._codereview_impl.GetMostRecentPatchset()
1571
1572 def __getattr__(self, attr):
1573 # This is because lots of untested code accesses Rietveld-specific stuff
1574 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001575 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001576 # Note that child method defines __getattr__ as well, and forwards it here,
1577 # because _RietveldChangelistImpl is not cleaned up yet, and given
1578 # deprecation of Rietveld, it should probably be just removed.
1579 # Until that time, avoid infinite recursion by bypassing __getattr__
1580 # of implementation class.
1581 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001582
1583
1584class _ChangelistCodereviewBase(object):
1585 """Abstract base class encapsulating codereview specifics of a changelist."""
1586 def __init__(self, changelist):
1587 self._changelist = changelist # instance of Changelist
1588
1589 def __getattr__(self, attr):
1590 # Forward methods to changelist.
1591 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1592 # _RietveldChangelistImpl to avoid this hack?
1593 return getattr(self._changelist, attr)
1594
1595 def GetStatus(self):
1596 """Apply a rough heuristic to give a simple summary of an issue's review
1597 or CQ status, assuming adherence to a common workflow.
1598
1599 Returns None if no issue for this branch, or specific string keywords.
1600 """
1601 raise NotImplementedError()
1602
1603 def GetCodereviewServer(self):
1604 """Returns server URL without end slash, like "https://codereview.com"."""
1605 raise NotImplementedError()
1606
1607 def FetchDescription(self):
1608 """Fetches and returns description from the codereview server."""
1609 raise NotImplementedError()
1610
tandrii5d48c322016-08-18 16:19:37 -07001611 @classmethod
1612 def IssueConfigKey(cls):
1613 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001614 raise NotImplementedError()
1615
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001616 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001617 def PatchsetConfigKey(cls):
1618 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001619 raise NotImplementedError()
1620
tandrii5d48c322016-08-18 16:19:37 -07001621 @classmethod
1622 def CodereviewServerConfigKey(cls):
1623 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001624 raise NotImplementedError()
1625
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001626 def _PostUnsetIssueProperties(self):
1627 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001628 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001629
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001630 def GetRieveldObjForPresubmit(self):
1631 # This is an unfortunate Rietveld-embeddedness in presubmit.
1632 # For non-Rietveld codereviews, this probably should return a dummy object.
1633 raise NotImplementedError()
1634
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001635 def GetGerritObjForPresubmit(self):
1636 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1637 return None
1638
dsansomee2d6fd92016-09-08 00:10:47 -07001639 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001640 """Update the description on codereview site."""
1641 raise NotImplementedError()
1642
1643 def CloseIssue(self):
1644 """Closes the issue."""
1645 raise NotImplementedError()
1646
1647 def GetApprovingReviewers(self):
1648 """Returns a list of reviewers approving the change.
1649
1650 Note: not necessarily committers.
1651 """
1652 raise NotImplementedError()
1653
1654 def GetMostRecentPatchset(self):
1655 """Returns the most recent patchset number from the codereview site."""
1656 raise NotImplementedError()
1657
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001658 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1659 directory):
1660 """Fetches and applies the issue.
1661
1662 Arguments:
1663 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1664 reject: if True, reject the failed patch instead of switching to 3-way
1665 merge. Rietveld only.
1666 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1667 only.
1668 directory: switch to directory before applying the patch. Rietveld only.
1669 """
1670 raise NotImplementedError()
1671
1672 @staticmethod
1673 def ParseIssueURL(parsed_url):
1674 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1675 failed."""
1676 raise NotImplementedError()
1677
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001678 def EnsureAuthenticated(self, force):
1679 """Best effort check that user is authenticated with codereview server.
1680
1681 Arguments:
1682 force: whether to skip confirmation questions.
1683 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001684 raise NotImplementedError()
1685
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001686 def CMDUploadChange(self, options, args, change):
1687 """Uploads a change to codereview."""
1688 raise NotImplementedError()
1689
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001690 def SetCQState(self, new_state):
1691 """Update the CQ state for latest patchset.
1692
1693 Issue must have been already uploaded and known.
1694 """
1695 raise NotImplementedError()
1696
tandriie113dfd2016-10-11 10:20:12 -07001697 def CannotTriggerTryJobReason(self):
1698 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1699 raise NotImplementedError()
1700
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001701
1702class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1703 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1704 super(_RietveldChangelistImpl, self).__init__(changelist)
1705 assert settings, 'must be initialized in _ChangelistCodereviewBase'
martiniss6eda05f2016-06-30 10:18:35 -07001706 if not rietveld_server:
1707 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001708
1709 self._rietveld_server = rietveld_server
1710 self._auth_config = auth_config
1711 self._props = None
1712 self._rpc_server = None
1713
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001714 def GetCodereviewServer(self):
1715 if not self._rietveld_server:
1716 # If we're on a branch then get the server potentially associated
1717 # with that branch.
1718 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001719 self._rietveld_server = gclient_utils.UpgradeToHttps(
1720 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001721 if not self._rietveld_server:
1722 self._rietveld_server = settings.GetDefaultServerUrl()
1723 return self._rietveld_server
1724
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001725 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001726 """Best effort check that user is authenticated with Rietveld server."""
1727 if self._auth_config.use_oauth2:
1728 authenticator = auth.get_authenticator_for_host(
1729 self.GetCodereviewServer(), self._auth_config)
1730 if not authenticator.has_cached_credentials():
1731 raise auth.LoginRequiredError(self.GetCodereviewServer())
1732
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001733 def FetchDescription(self):
1734 issue = self.GetIssue()
1735 assert issue
1736 try:
1737 return self.RpcServer().get_description(issue).strip()
1738 except urllib2.HTTPError as e:
1739 if e.code == 404:
1740 DieWithError(
1741 ('\nWhile fetching the description for issue %d, received a '
1742 '404 (not found)\n'
1743 'error. It is likely that you deleted this '
1744 'issue on the server. If this is the\n'
1745 'case, please run\n\n'
1746 ' git cl issue 0\n\n'
1747 'to clear the association with the deleted issue. Then run '
1748 'this command again.') % issue)
1749 else:
1750 DieWithError(
1751 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1752 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001753 print('Warning: Failed to retrieve CL description due to network '
1754 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001755 return ''
1756
1757 def GetMostRecentPatchset(self):
1758 return self.GetIssueProperties()['patchsets'][-1]
1759
1760 def GetPatchSetDiff(self, issue, patchset):
1761 return self.RpcServer().get(
1762 '/download/issue%s_%s.diff' % (issue, patchset))
1763
1764 def GetIssueProperties(self):
1765 if self._props is None:
1766 issue = self.GetIssue()
1767 if not issue:
1768 self._props = {}
1769 else:
1770 self._props = self.RpcServer().get_issue_properties(issue, True)
1771 return self._props
1772
tandriie113dfd2016-10-11 10:20:12 -07001773 def CannotTriggerTryJobReason(self):
1774 props = self.GetIssueProperties()
1775 if not props:
1776 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1777 if props.get('closed'):
1778 return 'CL %s is closed' % self.GetIssue()
1779 if props.get('private'):
1780 return 'CL %s is private' % self.GetIssue()
1781 return None
1782
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001783 def GetApprovingReviewers(self):
1784 return get_approving_reviewers(self.GetIssueProperties())
1785
1786 def AddComment(self, message):
1787 return self.RpcServer().add_comment(self.GetIssue(), message)
1788
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001789 def GetStatus(self):
1790 """Apply a rough heuristic to give a simple summary of an issue's review
1791 or CQ status, assuming adherence to a common workflow.
1792
1793 Returns None if no issue for this branch, or one of the following keywords:
1794 * 'error' - error from review tool (including deleted issues)
1795 * 'unsent' - not sent for review
1796 * 'waiting' - waiting for review
1797 * 'reply' - waiting for owner to reply to review
1798 * 'lgtm' - LGTM from at least one approved reviewer
1799 * 'commit' - in the commit queue
1800 * 'closed' - closed
1801 """
1802 if not self.GetIssue():
1803 return None
1804
1805 try:
1806 props = self.GetIssueProperties()
1807 except urllib2.HTTPError:
1808 return 'error'
1809
1810 if props.get('closed'):
1811 # Issue is closed.
1812 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001813 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001814 # Issue is in the commit queue.
1815 return 'commit'
1816
1817 try:
1818 reviewers = self.GetApprovingReviewers()
1819 except urllib2.HTTPError:
1820 return 'error'
1821
1822 if reviewers:
1823 # Was LGTM'ed.
1824 return 'lgtm'
1825
1826 messages = props.get('messages') or []
1827
tandrii9d2c7a32016-06-22 03:42:45 -07001828 # Skip CQ messages that don't require owner's action.
1829 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1830 if 'Dry run:' in messages[-1]['text']:
1831 messages.pop()
1832 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1833 # This message always follows prior messages from CQ,
1834 # so skip this too.
1835 messages.pop()
1836 else:
1837 # This is probably a CQ messages warranting user attention.
1838 break
1839
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001840 if not messages:
1841 # No message was sent.
1842 return 'unsent'
1843 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07001844 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001845 return 'reply'
1846 return 'waiting'
1847
dsansomee2d6fd92016-09-08 00:10:47 -07001848 def UpdateDescriptionRemote(self, description, force=False):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001849 return self.RpcServer().update_description(
1850 self.GetIssue(), self.description)
1851
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001852 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001853 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001854
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001855 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07001856 return self.SetFlags({flag: value})
1857
1858 def SetFlags(self, flags):
1859 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07001860 """
phajdan.jr68598232016-08-10 03:28:28 -07001861 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001862 try:
tandrii4b233bd2016-07-06 03:50:29 -07001863 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07001864 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07001865 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001866 if e.code == 404:
1867 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1868 if e.code == 403:
1869 DieWithError(
1870 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07001871 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001872 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001873
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001874 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001875 """Returns an upload.RpcServer() to access this review's rietveld instance.
1876 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001877 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001878 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001879 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001880 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001881 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001882
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001883 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001884 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001885 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001886
tandrii5d48c322016-08-18 16:19:37 -07001887 @classmethod
1888 def PatchsetConfigKey(cls):
1889 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001890
tandrii5d48c322016-08-18 16:19:37 -07001891 @classmethod
1892 def CodereviewServerConfigKey(cls):
1893 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001894
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001895 def GetRieveldObjForPresubmit(self):
1896 return self.RpcServer()
1897
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001898 def SetCQState(self, new_state):
1899 props = self.GetIssueProperties()
1900 if props.get('private'):
1901 DieWithError('Cannot set-commit on private issue')
1902
1903 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07001904 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001905 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07001906 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001907 else:
tandrii4b233bd2016-07-06 03:50:29 -07001908 assert new_state == _CQState.DRY_RUN
1909 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001910
1911
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001912 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1913 directory):
1914 # TODO(maruel): Use apply_issue.py
1915
1916 # PatchIssue should never be called with a dirty tree. It is up to the
1917 # caller to check this, but just in case we assert here since the
1918 # consequences of the caller not checking this could be dire.
1919 assert(not git_common.is_dirty_git_tree('apply'))
1920 assert(parsed_issue_arg.valid)
1921 self._changelist.issue = parsed_issue_arg.issue
1922 if parsed_issue_arg.hostname:
1923 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1924
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001925 if (isinstance(parsed_issue_arg, _RietveldParsedIssueNumberArgument) and
1926 parsed_issue_arg.patch_url):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001927 assert parsed_issue_arg.patchset
1928 patchset = parsed_issue_arg.patchset
1929 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1930 else:
1931 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1932 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1933
1934 # Switch up to the top-level directory, if necessary, in preparation for
1935 # applying the patch.
1936 top = settings.GetRelativeRoot()
1937 if top:
1938 os.chdir(top)
1939
1940 # Git patches have a/ at the beginning of source paths. We strip that out
1941 # with a sed script rather than the -p flag to patch so we can feed either
1942 # Git or svn-style patches into the same apply command.
1943 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1944 try:
1945 patch_data = subprocess2.check_output(
1946 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1947 except subprocess2.CalledProcessError:
1948 DieWithError('Git patch mungling failed.')
1949 logging.info(patch_data)
1950
1951 # We use "git apply" to apply the patch instead of "patch" so that we can
1952 # pick up file adds.
1953 # The --index flag means: also insert into the index (so we catch adds).
1954 cmd = ['git', 'apply', '--index', '-p0']
1955 if directory:
1956 cmd.extend(('--directory', directory))
1957 if reject:
1958 cmd.append('--reject')
1959 elif IsGitVersionAtLeast('1.7.12'):
1960 cmd.append('--3way')
1961 try:
1962 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
1963 stdin=patch_data, stdout=subprocess2.VOID)
1964 except subprocess2.CalledProcessError:
vapiera7fbd5a2016-06-16 09:17:49 -07001965 print('Failed to apply the patch')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001966 return 1
1967
1968 # If we had an issue, commit the current state and register the issue.
1969 if not nocommit:
1970 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
1971 'patch from issue %(i)s at patchset '
1972 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
1973 % {'i': self.GetIssue(), 'p': patchset})])
1974 self.SetIssue(self.GetIssue())
1975 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07001976 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001977 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001978 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001979 return 0
1980
1981 @staticmethod
1982 def ParseIssueURL(parsed_url):
1983 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1984 return None
wychen3c1c1722016-08-04 11:46:36 -07001985 # Rietveld patch: https://domain/<number>/#ps<patchset>
1986 match = re.match(r'/(\d+)/$', parsed_url.path)
1987 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
1988 if match and match2:
1989 return _RietveldParsedIssueNumberArgument(
1990 issue=int(match.group(1)),
1991 patchset=int(match2.group(1)),
1992 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001993 # Typical url: https://domain/<issue_number>[/[other]]
1994 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
1995 if match:
1996 return _RietveldParsedIssueNumberArgument(
1997 issue=int(match.group(1)),
1998 hostname=parsed_url.netloc)
1999 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2000 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2001 if match:
2002 return _RietveldParsedIssueNumberArgument(
2003 issue=int(match.group(1)),
2004 patchset=int(match.group(2)),
2005 hostname=parsed_url.netloc,
2006 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
2007 return None
2008
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002009 def CMDUploadChange(self, options, args, change):
2010 """Upload the patch to Rietveld."""
2011 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2012 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002013 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2014 if options.emulate_svn_auto_props:
2015 upload_args.append('--emulate_svn_auto_props')
2016
2017 change_desc = None
2018
2019 if options.email is not None:
2020 upload_args.extend(['--email', options.email])
2021
2022 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002023 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002024 upload_args.extend(['--title', options.title])
2025 if options.message:
2026 upload_args.extend(['--message', options.message])
2027 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002028 print('This branch is associated with issue %s. '
2029 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002030 else:
nodirca166002016-06-27 10:59:51 -07002031 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002032 upload_args.extend(['--title', options.title])
2033 message = (options.title or options.message or
2034 CreateDescriptionFromLog(args))
2035 change_desc = ChangeDescription(message)
2036 if options.reviewers or options.tbr_owners:
2037 change_desc.update_reviewers(options.reviewers,
2038 options.tbr_owners,
2039 change)
2040 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002041 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002042
2043 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002044 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002045 return 1
2046
2047 upload_args.extend(['--message', change_desc.description])
2048 if change_desc.get_reviewers():
2049 upload_args.append('--reviewers=%s' % ','.join(
2050 change_desc.get_reviewers()))
2051 if options.send_mail:
2052 if not change_desc.get_reviewers():
2053 DieWithError("Must specify reviewers to send email.")
2054 upload_args.append('--send_mail')
2055
2056 # We check this before applying rietveld.private assuming that in
2057 # rietveld.cc only addresses which we can send private CLs to are listed
2058 # if rietveld.private is set, and so we should ignore rietveld.cc only
2059 # when --private is specified explicitly on the command line.
2060 if options.private:
2061 logging.warn('rietveld.cc is ignored since private flag is specified. '
2062 'You need to review and add them manually if necessary.')
2063 cc = self.GetCCListWithoutDefault()
2064 else:
2065 cc = self.GetCCList()
2066 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
2067 if cc:
2068 upload_args.extend(['--cc', cc])
2069
2070 if options.private or settings.GetDefaultPrivateFlag() == "True":
2071 upload_args.append('--private')
2072
2073 upload_args.extend(['--git_similarity', str(options.similarity)])
2074 if not options.find_copies:
2075 upload_args.extend(['--git_no_find_copies'])
2076
2077 # Include the upstream repo's URL in the change -- this is useful for
2078 # projects that have their source spread across multiple repos.
2079 remote_url = self.GetGitBaseUrlFromConfig()
2080 if not remote_url:
2081 if settings.GetIsGitSvn():
2082 remote_url = self.GetGitSvnRemoteUrl()
2083 else:
2084 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2085 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2086 self.GetUpstreamBranch().split('/')[-1])
2087 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002088 remote, remote_branch = self.GetRemoteBranch()
2089 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2090 settings.GetPendingRefPrefix())
2091 if target_ref:
2092 upload_args.extend(['--target_ref', target_ref])
2093
2094 # Look for dependent patchsets. See crbug.com/480453 for more details.
2095 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2096 upstream_branch = ShortBranchName(upstream_branch)
2097 if remote is '.':
2098 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002099 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002100 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002101 print()
2102 print('Skipping dependency patchset upload because git config '
2103 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2104 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002105 else:
2106 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002107 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002108 auth_config=auth_config)
2109 branch_cl_issue_url = branch_cl.GetIssueURL()
2110 branch_cl_issue = branch_cl.GetIssue()
2111 branch_cl_patchset = branch_cl.GetPatchset()
2112 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2113 upload_args.extend(
2114 ['--depends_on_patchset', '%s:%s' % (
2115 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002116 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002117 '\n'
2118 'The current branch (%s) is tracking a local branch (%s) with '
2119 'an associated CL.\n'
2120 'Adding %s/#ps%s as a dependency patchset.\n'
2121 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2122 branch_cl_patchset))
2123
2124 project = settings.GetProject()
2125 if project:
2126 upload_args.extend(['--project', project])
2127
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002128 try:
2129 upload_args = ['upload'] + upload_args + args
2130 logging.info('upload.RealMain(%s)', upload_args)
2131 issue, patchset = upload.RealMain(upload_args)
2132 issue = int(issue)
2133 patchset = int(patchset)
2134 except KeyboardInterrupt:
2135 sys.exit(1)
2136 except:
2137 # If we got an exception after the user typed a description for their
2138 # change, back up the description before re-raising.
2139 if change_desc:
2140 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2141 print('\nGot exception while uploading -- saving description to %s\n' %
2142 backup_path)
2143 backup_file = open(backup_path, 'w')
2144 backup_file.write(change_desc.description)
2145 backup_file.close()
2146 raise
2147
2148 if not self.GetIssue():
2149 self.SetIssue(issue)
2150 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002151 return 0
2152
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002153
2154class _GerritChangelistImpl(_ChangelistCodereviewBase):
2155 def __init__(self, changelist, auth_config=None):
2156 # auth_config is Rietveld thing, kept here to preserve interface only.
2157 super(_GerritChangelistImpl, self).__init__(changelist)
2158 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002159 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002160 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002161 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002162
2163 def _GetGerritHost(self):
2164 # Lazy load of configs.
2165 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002166 if self._gerrit_host and '.' not in self._gerrit_host:
2167 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2168 # This happens for internal stuff http://crbug.com/614312.
2169 parsed = urlparse.urlparse(self.GetRemoteUrl())
2170 if parsed.scheme == 'sso':
2171 print('WARNING: using non https URLs for remote is likely broken\n'
2172 ' Your current remote is: %s' % self.GetRemoteUrl())
2173 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2174 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002175 return self._gerrit_host
2176
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002177 def _GetGitHost(self):
2178 """Returns git host to be used when uploading change to Gerrit."""
2179 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2180
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002181 def GetCodereviewServer(self):
2182 if not self._gerrit_server:
2183 # If we're on a branch then get the server potentially associated
2184 # with that branch.
2185 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002186 self._gerrit_server = self._GitGetBranchConfigValue(
2187 self.CodereviewServerConfigKey())
2188 if self._gerrit_server:
2189 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002190 if not self._gerrit_server:
2191 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2192 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002193 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002194 parts[0] = parts[0] + '-review'
2195 self._gerrit_host = '.'.join(parts)
2196 self._gerrit_server = 'https://%s' % self._gerrit_host
2197 return self._gerrit_server
2198
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002199 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002200 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002201 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002202
tandrii5d48c322016-08-18 16:19:37 -07002203 @classmethod
2204 def PatchsetConfigKey(cls):
2205 return 'gerritpatchset'
2206
2207 @classmethod
2208 def CodereviewServerConfigKey(cls):
2209 return 'gerritserver'
2210
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002211 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002212 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002213 if settings.GetGerritSkipEnsureAuthenticated():
2214 # For projects with unusual authentication schemes.
2215 # See http://crbug.com/603378.
2216 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002217 # Lazy-loader to identify Gerrit and Git hosts.
2218 if gerrit_util.GceAuthenticator.is_gce():
2219 return
2220 self.GetCodereviewServer()
2221 git_host = self._GetGitHost()
2222 assert self._gerrit_server and self._gerrit_host
2223 cookie_auth = gerrit_util.CookiesAuthenticator()
2224
2225 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2226 git_auth = cookie_auth.get_auth_header(git_host)
2227 if gerrit_auth and git_auth:
2228 if gerrit_auth == git_auth:
2229 return
2230 print((
2231 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2232 ' Check your %s or %s file for credentials of hosts:\n'
2233 ' %s\n'
2234 ' %s\n'
2235 ' %s') %
2236 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2237 git_host, self._gerrit_host,
2238 cookie_auth.get_new_password_message(git_host)))
2239 if not force:
2240 ask_for_data('If you know what you are doing, press Enter to continue, '
2241 'Ctrl+C to abort.')
2242 return
2243 else:
2244 missing = (
2245 [] if gerrit_auth else [self._gerrit_host] +
2246 [] if git_auth else [git_host])
2247 DieWithError('Credentials for the following hosts are required:\n'
2248 ' %s\n'
2249 'These are read from %s (or legacy %s)\n'
2250 '%s' % (
2251 '\n '.join(missing),
2252 cookie_auth.get_gitcookies_path(),
2253 cookie_auth.get_netrc_path(),
2254 cookie_auth.get_new_password_message(git_host)))
2255
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002256 def _PostUnsetIssueProperties(self):
2257 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002258 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002259
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002260 def GetRieveldObjForPresubmit(self):
2261 class ThisIsNotRietveldIssue(object):
2262 def __nonzero__(self):
2263 # This is a hack to make presubmit_support think that rietveld is not
2264 # defined, yet still ensure that calls directly result in a decent
2265 # exception message below.
2266 return False
2267
2268 def __getattr__(self, attr):
2269 print(
2270 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2271 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2272 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2273 'or use Rietveld for codereview.\n'
2274 'See also http://crbug.com/579160.' % attr)
2275 raise NotImplementedError()
2276 return ThisIsNotRietveldIssue()
2277
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002278 def GetGerritObjForPresubmit(self):
2279 return presubmit_support.GerritAccessor(self._GetGerritHost())
2280
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002281 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002282 """Apply a rough heuristic to give a simple summary of an issue's review
2283 or CQ status, assuming adherence to a common workflow.
2284
2285 Returns None if no issue for this branch, or one of the following keywords:
2286 * 'error' - error from review tool (including deleted issues)
2287 * 'unsent' - no reviewers added
2288 * 'waiting' - waiting for review
2289 * 'reply' - waiting for owner to reply to review
tandriic2405f52016-10-10 08:13:15 -07002290 * 'not lgtm' - Code-Review disaproval from at least one valid reviewer
2291 * 'lgtm' - Code-Review approval from at least one valid reviewer
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002292 * 'commit' - in the commit queue
2293 * 'closed' - abandoned
2294 """
2295 if not self.GetIssue():
2296 return None
2297
2298 try:
2299 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
tandriic2405f52016-10-10 08:13:15 -07002300 except (httplib.HTTPException, GerritIssueNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002301 return 'error'
2302
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002303 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002304 return 'closed'
2305
2306 cq_label = data['labels'].get('Commit-Queue', {})
2307 if cq_label:
2308 # Vote value is a stringified integer, which we expect from 0 to 2.
2309 vote_value = cq_label.get('value', '0')
2310 vote_text = cq_label.get('values', {}).get(vote_value, '')
2311 if vote_text.lower() == 'commit':
2312 return 'commit'
2313
2314 lgtm_label = data['labels'].get('Code-Review', {})
2315 if lgtm_label:
2316 if 'rejected' in lgtm_label:
2317 return 'not lgtm'
2318 if 'approved' in lgtm_label:
2319 return 'lgtm'
2320
2321 if not data.get('reviewers', {}).get('REVIEWER', []):
2322 return 'unsent'
2323
2324 messages = data.get('messages', [])
2325 if messages:
2326 owner = data['owner'].get('_account_id')
2327 last_message_author = messages[-1].get('author', {}).get('_account_id')
2328 if owner != last_message_author:
2329 # Some reply from non-owner.
2330 return 'reply'
2331
2332 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002333
2334 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002335 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002336 return data['revisions'][data['current_revision']]['_number']
2337
2338 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002339 data = self._GetChangeDetail(['CURRENT_REVISION'])
2340 current_rev = data['current_revision']
2341 url = data['revisions'][current_rev]['fetch']['http']['url']
2342 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002343
dsansomee2d6fd92016-09-08 00:10:47 -07002344 def UpdateDescriptionRemote(self, description, force=False):
2345 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2346 if not force:
2347 ask_for_data(
2348 'The description cannot be modified while the issue has a pending '
2349 'unpublished edit. Either publish the edit in the Gerrit web UI '
2350 'or delete it.\n\n'
2351 'Press Enter to delete the unpublished edit, Ctrl+C to abort.')
2352
2353 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2354 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002355 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
2356 description)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002357
2358 def CloseIssue(self):
2359 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2360
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002361 def GetApprovingReviewers(self):
2362 """Returns a list of reviewers approving the change.
2363
2364 Note: not necessarily committers.
2365 """
2366 raise NotImplementedError()
2367
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002368 def SubmitIssue(self, wait_for_merge=True):
2369 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2370 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002371
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002372 def _GetChangeDetail(self, options=None, issue=None):
2373 options = options or []
2374 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002375 assert issue, 'issue is required to query Gerrit'
2376 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002377 options)
tandriic2405f52016-10-10 08:13:15 -07002378 if not data:
2379 raise GerritIssueNotExists(issue, self.GetCodereviewServer())
2380 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002381
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002382 def CMDLand(self, force, bypass_hooks, verbose):
2383 if git_common.is_dirty_git_tree('land'):
2384 return 1
tandriid60367b2016-06-22 05:25:12 -07002385 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2386 if u'Commit-Queue' in detail.get('labels', {}):
2387 if not force:
2388 ask_for_data('\nIt seems this repository has a Commit Queue, '
2389 'which can test and land changes for you. '
2390 'Are you sure you wish to bypass it?\n'
2391 'Press Enter to continue, Ctrl+C to abort.')
2392
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002393 differs = True
tandriic4344b52016-08-29 06:04:54 -07002394 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002395 # Note: git diff outputs nothing if there is no diff.
2396 if not last_upload or RunGit(['diff', last_upload]).strip():
2397 print('WARNING: some changes from local branch haven\'t been uploaded')
2398 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002399 if detail['current_revision'] == last_upload:
2400 differs = False
2401 else:
2402 print('WARNING: local branch contents differ from latest uploaded '
2403 'patchset')
2404 if differs:
2405 if not force:
2406 ask_for_data(
2407 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2408 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2409 elif not bypass_hooks:
2410 hook_results = self.RunHook(
2411 committing=True,
2412 may_prompt=not force,
2413 verbose=verbose,
2414 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2415 if not hook_results.should_continue():
2416 return 1
2417
2418 self.SubmitIssue(wait_for_merge=True)
2419 print('Issue %s has been submitted.' % self.GetIssueURL())
2420 return 0
2421
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002422 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2423 directory):
2424 assert not reject
2425 assert not nocommit
2426 assert not directory
2427 assert parsed_issue_arg.valid
2428
2429 self._changelist.issue = parsed_issue_arg.issue
2430
2431 if parsed_issue_arg.hostname:
2432 self._gerrit_host = parsed_issue_arg.hostname
2433 self._gerrit_server = 'https://%s' % self._gerrit_host
2434
tandriic2405f52016-10-10 08:13:15 -07002435 try:
2436 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2437 except GerritIssueNotExists as e:
2438 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002439
2440 if not parsed_issue_arg.patchset:
2441 # Use current revision by default.
2442 revision_info = detail['revisions'][detail['current_revision']]
2443 patchset = int(revision_info['_number'])
2444 else:
2445 patchset = parsed_issue_arg.patchset
2446 for revision_info in detail['revisions'].itervalues():
2447 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2448 break
2449 else:
2450 DieWithError('Couldn\'t find patchset %i in issue %i' %
2451 (parsed_issue_arg.patchset, self.GetIssue()))
2452
2453 fetch_info = revision_info['fetch']['http']
2454 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2455 RunGit(['cherry-pick', 'FETCH_HEAD'])
2456 self.SetIssue(self.GetIssue())
2457 self.SetPatchset(patchset)
2458 print('Committed patch for issue %i pathset %i locally' %
2459 (self.GetIssue(), self.GetPatchset()))
2460 return 0
2461
2462 @staticmethod
2463 def ParseIssueURL(parsed_url):
2464 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2465 return None
2466 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2467 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2468 # Short urls like https://domain/<issue_number> can be used, but don't allow
2469 # specifying the patchset (you'd 404), but we allow that here.
2470 if parsed_url.path == '/':
2471 part = parsed_url.fragment
2472 else:
2473 part = parsed_url.path
2474 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2475 if match:
2476 return _ParsedIssueNumberArgument(
2477 issue=int(match.group(2)),
2478 patchset=int(match.group(4)) if match.group(4) else None,
2479 hostname=parsed_url.netloc)
2480 return None
2481
tandrii16e0b4e2016-06-07 10:34:28 -07002482 def _GerritCommitMsgHookCheck(self, offer_removal):
2483 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2484 if not os.path.exists(hook):
2485 return
2486 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2487 # custom developer made one.
2488 data = gclient_utils.FileRead(hook)
2489 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2490 return
2491 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002492 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002493 'and may interfere with it in subtle ways.\n'
2494 'We recommend you remove the commit-msg hook.')
2495 if offer_removal:
2496 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2497 if reply.lower().startswith('y'):
2498 gclient_utils.rm_file_or_tree(hook)
2499 print('Gerrit commit-msg hook removed.')
2500 else:
2501 print('OK, will keep Gerrit commit-msg hook in place.')
2502
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002503 def CMDUploadChange(self, options, args, change):
2504 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002505 if options.squash and options.no_squash:
2506 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002507
2508 if not options.squash and not options.no_squash:
2509 # Load default for user, repo, squash=true, in this order.
2510 options.squash = settings.GetSquashGerritUploads()
2511 elif options.no_squash:
2512 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002513
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002514 # We assume the remote called "origin" is the one we want.
2515 # It is probably not worthwhile to support different workflows.
2516 gerrit_remote = 'origin'
2517
2518 remote, remote_branch = self.GetRemoteBranch()
2519 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2520 pending_prefix='')
2521
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002522 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002523 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002524 if self.GetIssue():
2525 # Try to get the message from a previous upload.
2526 message = self.GetDescription()
2527 if not message:
2528 DieWithError(
2529 'failed to fetch description from current Gerrit issue %d\n'
2530 '%s' % (self.GetIssue(), self.GetIssueURL()))
2531 change_id = self._GetChangeDetail()['change_id']
2532 while True:
2533 footer_change_ids = git_footers.get_footer_change_id(message)
2534 if footer_change_ids == [change_id]:
2535 break
2536 if not footer_change_ids:
2537 message = git_footers.add_footer_change_id(message, change_id)
2538 print('WARNING: appended missing Change-Id to issue description')
2539 continue
2540 # There is already a valid footer but with different or several ids.
2541 # Doing this automatically is non-trivial as we don't want to lose
2542 # existing other footers, yet we want to append just 1 desired
2543 # Change-Id. Thus, just create a new footer, but let user verify the
2544 # new description.
2545 message = '%s\n\nChange-Id: %s' % (message, change_id)
2546 print(
2547 'WARNING: issue %s has Change-Id footer(s):\n'
2548 ' %s\n'
2549 'but issue has Change-Id %s, according to Gerrit.\n'
2550 'Please, check the proposed correction to the description, '
2551 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2552 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2553 change_id))
2554 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2555 if not options.force:
2556 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002557 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002558 message = change_desc.description
2559 if not message:
2560 DieWithError("Description is empty. Aborting...")
2561 # Continue the while loop.
2562 # Sanity check of this code - we should end up with proper message
2563 # footer.
2564 assert [change_id] == git_footers.get_footer_change_id(message)
2565 change_desc = ChangeDescription(message)
2566 else:
2567 change_desc = ChangeDescription(
2568 options.message or CreateDescriptionFromLog(args))
2569 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002570 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002571 if not change_desc.description:
2572 DieWithError("Description is empty. Aborting...")
2573 message = change_desc.description
2574 change_ids = git_footers.get_footer_change_id(message)
2575 if len(change_ids) > 1:
2576 DieWithError('too many Change-Id footers, at most 1 allowed.')
2577 if not change_ids:
2578 # Generate the Change-Id automatically.
2579 message = git_footers.add_footer_change_id(
2580 message, GenerateGerritChangeId(message))
2581 change_desc.set_description(message)
2582 change_ids = git_footers.get_footer_change_id(message)
2583 assert len(change_ids) == 1
2584 change_id = change_ids[0]
2585
2586 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2587 if remote is '.':
2588 # If our upstream branch is local, we base our squashed commit on its
2589 # squashed version.
2590 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2591 # Check the squashed hash of the parent.
2592 parent = RunGit(['config',
2593 'branch.%s.gerritsquashhash' % upstream_branch_name],
2594 error_ok=True).strip()
2595 # Verify that the upstream branch has been uploaded too, otherwise
2596 # Gerrit will create additional CLs when uploading.
2597 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2598 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002599 DieWithError(
2600 'Upload upstream branch %s first.\n'
tandrii2bdadf12016-07-12 12:27:54 -07002601 'Note: maybe you\'ve uploaded it with --no-squash. '
2602 'If so, then re-upload it with:\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002603 ' git cl upload --squash\n' % upstream_branch_name)
2604 else:
2605 parent = self.GetCommonAncestorWithUpstream()
2606
2607 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2608 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2609 '-m', message]).strip()
2610 else:
2611 change_desc = ChangeDescription(
2612 options.message or CreateDescriptionFromLog(args))
2613 if not change_desc.description:
2614 DieWithError("Description is empty. Aborting...")
2615
2616 if not git_footers.get_footer_change_id(change_desc.description):
2617 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002618 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2619 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002620 ref_to_push = 'HEAD'
2621 parent = '%s/%s' % (gerrit_remote, branch)
2622 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2623
2624 assert change_desc
2625 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2626 ref_to_push)]).splitlines()
2627 if len(commits) > 1:
2628 print('WARNING: This will upload %d commits. Run the following command '
2629 'to see which commits will be uploaded: ' % len(commits))
2630 print('git log %s..%s' % (parent, ref_to_push))
2631 print('You can also use `git squash-branch` to squash these into a '
2632 'single commit.')
2633 ask_for_data('About to upload; enter to confirm.')
2634
2635 if options.reviewers or options.tbr_owners:
2636 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2637 change)
2638
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002639 # Extra options that can be specified at push time. Doc:
2640 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2641 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002642 if change_desc.get_reviewers(tbr_only=True):
2643 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2644 refspec_opts.append('l=Code-Review+1')
2645
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002646 if options.title:
tandriieefe8322016-08-17 10:12:24 -07002647 if not re.match(r'^[\w ]+$', options.title):
2648 options.title = re.sub(r'[^\w ]', '', options.title)
2649 print('WARNING: Patchset title may only contain alphanumeric chars '
2650 'and spaces. Cleaned up title:\n%s' % options.title)
2651 if not options.force:
2652 ask_for_data('Press enter to continue, Ctrl+C to abort')
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002653 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2654 # reverse on its side.
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002655 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2656
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002657 if options.send_mail:
2658 if not change_desc.get_reviewers():
2659 DieWithError('Must specify reviewers to send email.')
2660 refspec_opts.append('notify=ALL')
2661 else:
2662 refspec_opts.append('notify=NONE')
2663
tandrii99a72f22016-08-17 14:33:24 -07002664 reviewers = change_desc.get_reviewers()
2665 if reviewers:
2666 refspec_opts.extend('r=' + email.strip() for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002667
agablec6787972016-09-09 16:13:34 -07002668 if options.private:
2669 refspec_opts.append('draft')
2670
rmistry9eadede2016-09-19 11:22:43 -07002671 if options.topic:
2672 # Documentation on Gerrit topics is here:
2673 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
2674 refspec_opts.append('topic=%s' % options.topic)
2675
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002676 refspec_suffix = ''
2677 if refspec_opts:
2678 refspec_suffix = '%' + ','.join(refspec_opts)
2679 assert ' ' not in refspec_suffix, (
2680 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002681 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002682
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002683 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002684 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002685 print_stdout=True,
2686 # Flush after every line: useful for seeing progress when running as
2687 # recipe.
2688 filter_fn=lambda _: sys.stdout.flush())
2689
2690 if options.squash:
2691 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2692 change_numbers = [m.group(1)
2693 for m in map(regex.match, push_stdout.splitlines())
2694 if m]
2695 if len(change_numbers) != 1:
2696 DieWithError(
2697 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2698 'Change-Id: %s') % (len(change_numbers), change_id))
2699 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002700 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002701
2702 # Add cc's from the CC_LIST and --cc flag (if any).
2703 cc = self.GetCCList().split(',')
2704 if options.cc:
2705 cc.extend(options.cc)
2706 cc = filter(None, [email.strip() for email in cc])
2707 if cc:
2708 gerrit_util.AddReviewers(
2709 self._GetGerritHost(), self.GetIssue(), cc, is_reviewer=False)
2710
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002711 return 0
2712
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002713 def _AddChangeIdToCommitMessage(self, options, args):
2714 """Re-commits using the current message, assumes the commit hook is in
2715 place.
2716 """
2717 log_desc = options.message or CreateDescriptionFromLog(args)
2718 git_command = ['commit', '--amend', '-m', log_desc]
2719 RunGit(git_command)
2720 new_log_desc = CreateDescriptionFromLog(args)
2721 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002722 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002723 return new_log_desc
2724 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002725 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002726
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002727 def SetCQState(self, new_state):
2728 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002729 vote_map = {
2730 _CQState.NONE: 0,
2731 _CQState.DRY_RUN: 1,
2732 _CQState.COMMIT : 2,
2733 }
2734 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2735 labels={'Commit-Queue': vote_map[new_state]})
2736
tandriie113dfd2016-10-11 10:20:12 -07002737 def CannotTriggerTryJobReason(self):
2738 # TODO(tandrii): implement for Gerrit.
2739 raise NotImplementedError()
2740
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002741
2742_CODEREVIEW_IMPLEMENTATIONS = {
2743 'rietveld': _RietveldChangelistImpl,
2744 'gerrit': _GerritChangelistImpl,
2745}
2746
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002747
iannuccie53c9352016-08-17 14:40:40 -07002748def _add_codereview_issue_select_options(parser, extra=""):
2749 _add_codereview_select_options(parser)
2750
2751 text = ('Operate on this issue number instead of the current branch\'s '
2752 'implicit issue.')
2753 if extra:
2754 text += ' '+extra
2755 parser.add_option('-i', '--issue', type=int, help=text)
2756
2757
2758def _process_codereview_issue_select_options(parser, options):
2759 _process_codereview_select_options(parser, options)
2760 if options.issue is not None and not options.forced_codereview:
2761 parser.error('--issue must be specified with either --rietveld or --gerrit')
2762
2763
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002764def _add_codereview_select_options(parser):
2765 """Appends --gerrit and --rietveld options to force specific codereview."""
2766 parser.codereview_group = optparse.OptionGroup(
2767 parser, 'EXPERIMENTAL! Codereview override options')
2768 parser.add_option_group(parser.codereview_group)
2769 parser.codereview_group.add_option(
2770 '--gerrit', action='store_true',
2771 help='Force the use of Gerrit for codereview')
2772 parser.codereview_group.add_option(
2773 '--rietveld', action='store_true',
2774 help='Force the use of Rietveld for codereview')
2775
2776
2777def _process_codereview_select_options(parser, options):
2778 if options.gerrit and options.rietveld:
2779 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2780 options.forced_codereview = None
2781 if options.gerrit:
2782 options.forced_codereview = 'gerrit'
2783 elif options.rietveld:
2784 options.forced_codereview = 'rietveld'
2785
2786
tandriif9aefb72016-07-01 09:06:51 -07002787def _get_bug_line_values(default_project, bugs):
2788 """Given default_project and comma separated list of bugs, yields bug line
2789 values.
2790
2791 Each bug can be either:
2792 * a number, which is combined with default_project
2793 * string, which is left as is.
2794
2795 This function may produce more than one line, because bugdroid expects one
2796 project per line.
2797
2798 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2799 ['v8:123', 'chromium:789']
2800 """
2801 default_bugs = []
2802 others = []
2803 for bug in bugs.split(','):
2804 bug = bug.strip()
2805 if bug:
2806 try:
2807 default_bugs.append(int(bug))
2808 except ValueError:
2809 others.append(bug)
2810
2811 if default_bugs:
2812 default_bugs = ','.join(map(str, default_bugs))
2813 if default_project:
2814 yield '%s:%s' % (default_project, default_bugs)
2815 else:
2816 yield default_bugs
2817 for other in sorted(others):
2818 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2819 yield other
2820
2821
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002822class ChangeDescription(object):
2823 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002824 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002825 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002826
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002827 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002828 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002829
agable@chromium.org42c20792013-09-12 17:34:49 +00002830 @property # www.logilab.org/ticket/89786
2831 def description(self): # pylint: disable=E0202
2832 return '\n'.join(self._description_lines)
2833
2834 def set_description(self, desc):
2835 if isinstance(desc, basestring):
2836 lines = desc.splitlines()
2837 else:
2838 lines = [line.rstrip() for line in desc]
2839 while lines and not lines[0]:
2840 lines.pop(0)
2841 while lines and not lines[-1]:
2842 lines.pop(-1)
2843 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002844
piman@chromium.org336f9122014-09-04 02:16:55 +00002845 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002846 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002847 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002848 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002849 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002850 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002851
agable@chromium.org42c20792013-09-12 17:34:49 +00002852 # Get the set of R= and TBR= lines and remove them from the desciption.
2853 regexp = re.compile(self.R_LINE)
2854 matches = [regexp.match(line) for line in self._description_lines]
2855 new_desc = [l for i, l in enumerate(self._description_lines)
2856 if not matches[i]]
2857 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002858
agable@chromium.org42c20792013-09-12 17:34:49 +00002859 # Construct new unified R= and TBR= lines.
2860 r_names = []
2861 tbr_names = []
2862 for match in matches:
2863 if not match:
2864 continue
2865 people = cleanup_list([match.group(2).strip()])
2866 if match.group(1) == 'TBR':
2867 tbr_names.extend(people)
2868 else:
2869 r_names.extend(people)
2870 for name in r_names:
2871 if name not in reviewers:
2872 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002873 if add_owners_tbr:
2874 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07002875 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00002876 all_reviewers = set(tbr_names + reviewers)
2877 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2878 all_reviewers)
2879 tbr_names.extend(owners_db.reviewers_for(missing_files,
2880 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002881 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2882 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2883
2884 # Put the new lines in the description where the old first R= line was.
2885 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2886 if 0 <= line_loc < len(self._description_lines):
2887 if new_tbr_line:
2888 self._description_lines.insert(line_loc, new_tbr_line)
2889 if new_r_line:
2890 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002891 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002892 if new_r_line:
2893 self.append_footer(new_r_line)
2894 if new_tbr_line:
2895 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002896
tandriif9aefb72016-07-01 09:06:51 -07002897 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002898 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002899 self.set_description([
2900 '# Enter a description of the change.',
2901 '# This will be displayed on the codereview site.',
2902 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002903 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002904 '--------------------',
2905 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002906
agable@chromium.org42c20792013-09-12 17:34:49 +00002907 regexp = re.compile(self.BUG_LINE)
2908 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07002909 prefix = settings.GetBugPrefix()
2910 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
2911 for value in values:
2912 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
2913 self.append_footer('BUG=%s' % value)
2914
agable@chromium.org42c20792013-09-12 17:34:49 +00002915 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002916 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002917 if not content:
2918 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002919 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002920
2921 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002922 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2923 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002924 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002925 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002926
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002927 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00002928 """Adds a footer line to the description.
2929
2930 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
2931 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
2932 that Gerrit footers are always at the end.
2933 """
2934 parsed_footer_line = git_footers.parse_footer(line)
2935 if parsed_footer_line:
2936 # Line is a gerrit footer in the form: Footer-Key: any value.
2937 # Thus, must be appended observing Gerrit footer rules.
2938 self.set_description(
2939 git_footers.add_footer(self.description,
2940 key=parsed_footer_line[0],
2941 value=parsed_footer_line[1]))
2942 return
2943
2944 if not self._description_lines:
2945 self._description_lines.append(line)
2946 return
2947
2948 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
2949 if gerrit_footers:
2950 # git_footers.split_footers ensures that there is an empty line before
2951 # actual (gerrit) footers, if any. We have to keep it that way.
2952 assert top_lines and top_lines[-1] == ''
2953 top_lines, separator = top_lines[:-1], top_lines[-1:]
2954 else:
2955 separator = [] # No need for separator if there are no gerrit_footers.
2956
2957 prev_line = top_lines[-1] if top_lines else ''
2958 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
2959 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2960 top_lines.append('')
2961 top_lines.append(line)
2962 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002963
tandrii99a72f22016-08-17 14:33:24 -07002964 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002965 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002966 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07002967 reviewers = [match.group(2).strip()
2968 for match in matches
2969 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002970 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002971
2972
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002973def get_approving_reviewers(props):
2974 """Retrieves the reviewers that approved a CL from the issue properties with
2975 messages.
2976
2977 Note that the list may contain reviewers that are not committer, thus are not
2978 considered by the CQ.
2979 """
2980 return sorted(
2981 set(
2982 message['sender']
2983 for message in props['messages']
2984 if message['approval'] and message['sender'] in props['reviewers']
2985 )
2986 )
2987
2988
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002989def FindCodereviewSettingsFile(filename='codereview.settings'):
2990 """Finds the given file starting in the cwd and going up.
2991
2992 Only looks up to the top of the repository unless an
2993 'inherit-review-settings-ok' file exists in the root of the repository.
2994 """
2995 inherit_ok_file = 'inherit-review-settings-ok'
2996 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002997 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002998 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2999 root = '/'
3000 while True:
3001 if filename in os.listdir(cwd):
3002 if os.path.isfile(os.path.join(cwd, filename)):
3003 return open(os.path.join(cwd, filename))
3004 if cwd == root:
3005 break
3006 cwd = os.path.dirname(cwd)
3007
3008
3009def LoadCodereviewSettingsFromFile(fileobj):
3010 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003011 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003012
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003013 def SetProperty(name, setting, unset_error_ok=False):
3014 fullname = 'rietveld.' + name
3015 if setting in keyvals:
3016 RunGit(['config', fullname, keyvals[setting]])
3017 else:
3018 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3019
3020 SetProperty('server', 'CODE_REVIEW_SERVER')
3021 # Only server setting is required. Other settings can be absent.
3022 # In that case, we ignore errors raised during option deletion attempt.
3023 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003024 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003025 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3026 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003027 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003028 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003029 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
3030 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003031 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003032 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003033 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
tandriif46c20f2016-09-14 06:17:05 -07003034 SetProperty('git-number-footer', 'GIT_NUMBER_FOOTER', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003035 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3036 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003037
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003038 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003039 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003040
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003041 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003042 RunGit(['config', 'gerrit.squash-uploads',
3043 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003044
tandrii@chromium.org28253532016-04-14 13:46:56 +00003045 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003046 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003047 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3048
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003049 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
3050 #should be of the form
3051 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3052 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
3053 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3054 keyvals['ORIGIN_URL_CONFIG']])
3055
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003056
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003057def urlretrieve(source, destination):
3058 """urllib is broken for SSL connections via a proxy therefore we
3059 can't use urllib.urlretrieve()."""
3060 with open(destination, 'w') as f:
3061 f.write(urllib2.urlopen(source).read())
3062
3063
ukai@chromium.org712d6102013-11-27 00:52:58 +00003064def hasSheBang(fname):
3065 """Checks fname is a #! script."""
3066 with open(fname) as f:
3067 return f.read(2).startswith('#!')
3068
3069
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003070# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3071def DownloadHooks(*args, **kwargs):
3072 pass
3073
3074
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003075def DownloadGerritHook(force):
3076 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003077
3078 Args:
3079 force: True to update hooks. False to install hooks if not present.
3080 """
3081 if not settings.GetIsGerrit():
3082 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003083 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003084 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3085 if not os.access(dst, os.X_OK):
3086 if os.path.exists(dst):
3087 if not force:
3088 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003089 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003090 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003091 if not hasSheBang(dst):
3092 DieWithError('Not a script: %s\n'
3093 'You need to download from\n%s\n'
3094 'into .git/hooks/commit-msg and '
3095 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003096 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3097 except Exception:
3098 if os.path.exists(dst):
3099 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003100 DieWithError('\nFailed to download hooks.\n'
3101 'You need to download from\n%s\n'
3102 'into .git/hooks/commit-msg and '
3103 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003104
3105
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003106
3107def GetRietveldCodereviewSettingsInteractively():
3108 """Prompt the user for settings."""
3109 server = settings.GetDefaultServerUrl(error_ok=True)
3110 prompt = 'Rietveld server (host[:port])'
3111 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3112 newserver = ask_for_data(prompt + ':')
3113 if not server and not newserver:
3114 newserver = DEFAULT_SERVER
3115 if newserver:
3116 newserver = gclient_utils.UpgradeToHttps(newserver)
3117 if newserver != server:
3118 RunGit(['config', 'rietveld.server', newserver])
3119
3120 def SetProperty(initial, caption, name, is_url):
3121 prompt = caption
3122 if initial:
3123 prompt += ' ("x" to clear) [%s]' % initial
3124 new_val = ask_for_data(prompt + ':')
3125 if new_val == 'x':
3126 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3127 elif new_val:
3128 if is_url:
3129 new_val = gclient_utils.UpgradeToHttps(new_val)
3130 if new_val != initial:
3131 RunGit(['config', 'rietveld.' + name, new_val])
3132
3133 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3134 SetProperty(settings.GetDefaultPrivateFlag(),
3135 'Private flag (rietveld only)', 'private', False)
3136 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3137 'tree-status-url', False)
3138 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3139 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3140 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3141 'run-post-upload-hook', False)
3142
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003143@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003144def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003145 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003146
tandrii5d0a0422016-09-14 06:24:35 -07003147 print('WARNING: git cl config works for Rietveld only')
3148 # TODO(tandrii): remove this once we switch to Gerrit.
3149 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003150 parser.add_option('--activate-update', action='store_true',
3151 help='activate auto-updating [rietveld] section in '
3152 '.git/config')
3153 parser.add_option('--deactivate-update', action='store_true',
3154 help='deactivate auto-updating [rietveld] section in '
3155 '.git/config')
3156 options, args = parser.parse_args(args)
3157
3158 if options.deactivate_update:
3159 RunGit(['config', 'rietveld.autoupdate', 'false'])
3160 return
3161
3162 if options.activate_update:
3163 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3164 return
3165
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003166 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003167 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003168 return 0
3169
3170 url = args[0]
3171 if not url.endswith('codereview.settings'):
3172 url = os.path.join(url, 'codereview.settings')
3173
3174 # Load code review settings and download hooks (if available).
3175 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3176 return 0
3177
3178
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003179def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003180 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003181 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3182 branch = ShortBranchName(branchref)
3183 _, args = parser.parse_args(args)
3184 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003185 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003186 return RunGit(['config', 'branch.%s.base-url' % branch],
3187 error_ok=False).strip()
3188 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003189 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003190 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3191 error_ok=False).strip()
3192
3193
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003194def color_for_status(status):
3195 """Maps a Changelist status to color, for CMDstatus and other tools."""
3196 return {
3197 'unsent': Fore.RED,
3198 'waiting': Fore.BLUE,
3199 'reply': Fore.YELLOW,
3200 'lgtm': Fore.GREEN,
3201 'commit': Fore.MAGENTA,
3202 'closed': Fore.CYAN,
3203 'error': Fore.WHITE,
3204 }.get(status, Fore.WHITE)
3205
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003206
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003207def get_cl_statuses(changes, fine_grained, max_processes=None):
3208 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003209
3210 If fine_grained is true, this will fetch CL statuses from the server.
3211 Otherwise, simply indicate if there's a matching url for the given branches.
3212
3213 If max_processes is specified, it is used as the maximum number of processes
3214 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3215 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003216
3217 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003218 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003219 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003220 upload.verbosity = 0
3221
3222 if fine_grained:
3223 # Process one branch synchronously to work through authentication, then
3224 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003225 if changes:
tandriiea9514a2016-08-17 12:32:37 -07003226 def fetch(cl):
3227 try:
3228 return (cl, cl.GetStatus())
3229 except:
3230 # See http://crbug.com/629863.
3231 logging.exception('failed to fetch status for %s:', cl)
3232 raise
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003233 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003234
tandriiea9514a2016-08-17 12:32:37 -07003235 changes_to_fetch = changes[1:]
3236 if not changes_to_fetch:
kmarshall3bff56b2016-06-06 18:31:47 -07003237 # Exit early if there was only one branch to fetch.
3238 return
3239
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003240 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003241 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003242 if max_processes is not None
dsinclair99d30172016-08-09 10:48:58 -07003243 else max(len(changes_to_fetch), 1))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003244
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003245 fetched_cls = set()
3246 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003247 while True:
3248 try:
3249 row = it.next(timeout=5)
3250 except multiprocessing.TimeoutError:
3251 break
3252
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003253 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003254 yield row
3255
3256 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003257 for cl in set(changes_to_fetch) - fetched_cls:
3258 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003259
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003260 else:
3261 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003262 for cl in changes:
3263 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003264
rmistry@google.com2dd99862015-06-22 12:22:18 +00003265
3266def upload_branch_deps(cl, args):
3267 """Uploads CLs of local branches that are dependents of the current branch.
3268
3269 If the local branch dependency tree looks like:
3270 test1 -> test2.1 -> test3.1
3271 -> test3.2
3272 -> test2.2 -> test3.3
3273
3274 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3275 run on the dependent branches in this order:
3276 test2.1, test3.1, test3.2, test2.2, test3.3
3277
3278 Note: This function does not rebase your local dependent branches. Use it when
3279 you make a change to the parent branch that will not conflict with its
3280 dependent branches, and you would like their dependencies updated in
3281 Rietveld.
3282 """
3283 if git_common.is_dirty_git_tree('upload-branch-deps'):
3284 return 1
3285
3286 root_branch = cl.GetBranch()
3287 if root_branch is None:
3288 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3289 'Get on a branch!')
3290 if not cl.GetIssue() or not cl.GetPatchset():
3291 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3292 'patchset dependencies without an uploaded CL.')
3293
3294 branches = RunGit(['for-each-ref',
3295 '--format=%(refname:short) %(upstream:short)',
3296 'refs/heads'])
3297 if not branches:
3298 print('No local branches found.')
3299 return 0
3300
3301 # Create a dictionary of all local branches to the branches that are dependent
3302 # on it.
3303 tracked_to_dependents = collections.defaultdict(list)
3304 for b in branches.splitlines():
3305 tokens = b.split()
3306 if len(tokens) == 2:
3307 branch_name, tracked = tokens
3308 tracked_to_dependents[tracked].append(branch_name)
3309
vapiera7fbd5a2016-06-16 09:17:49 -07003310 print()
3311 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003312 dependents = []
3313 def traverse_dependents_preorder(branch, padding=''):
3314 dependents_to_process = tracked_to_dependents.get(branch, [])
3315 padding += ' '
3316 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003317 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003318 dependents.append(dependent)
3319 traverse_dependents_preorder(dependent, padding)
3320 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003321 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003322
3323 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003324 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003325 return 0
3326
vapiera7fbd5a2016-06-16 09:17:49 -07003327 print('This command will checkout all dependent branches and run '
3328 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003329 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3330
andybons@chromium.org962f9462016-02-03 20:00:42 +00003331 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003332 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003333 args.extend(['-t', 'Updated patchset dependency'])
3334
rmistry@google.com2dd99862015-06-22 12:22:18 +00003335 # Record all dependents that failed to upload.
3336 failures = {}
3337 # Go through all dependents, checkout the branch and upload.
3338 try:
3339 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003340 print()
3341 print('--------------------------------------')
3342 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003343 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003344 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003345 try:
3346 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003347 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003348 failures[dependent_branch] = 1
3349 except: # pylint: disable=W0702
3350 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003351 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003352 finally:
3353 # Swap back to the original root branch.
3354 RunGit(['checkout', '-q', root_branch])
3355
vapiera7fbd5a2016-06-16 09:17:49 -07003356 print()
3357 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003358 for dependent_branch in dependents:
3359 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003360 print(' %s : %s' % (dependent_branch, upload_status))
3361 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003362
3363 return 0
3364
3365
kmarshall3bff56b2016-06-06 18:31:47 -07003366def CMDarchive(parser, args):
3367 """Archives and deletes branches associated with closed changelists."""
3368 parser.add_option(
3369 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003370 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003371 parser.add_option(
3372 '-f', '--force', action='store_true',
3373 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003374 parser.add_option(
3375 '-d', '--dry-run', action='store_true',
3376 help='Skip the branch tagging and removal steps.')
3377 parser.add_option(
3378 '-t', '--notags', action='store_true',
3379 help='Do not tag archived branches. '
3380 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003381
3382 auth.add_auth_options(parser)
3383 options, args = parser.parse_args(args)
3384 if args:
3385 parser.error('Unsupported args: %s' % ' '.join(args))
3386 auth_config = auth.extract_auth_config_from_options(options)
3387
3388 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3389 if not branches:
3390 return 0
3391
vapiera7fbd5a2016-06-16 09:17:49 -07003392 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003393 changes = [Changelist(branchref=b, auth_config=auth_config)
3394 for b in branches.splitlines()]
3395 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3396 statuses = get_cl_statuses(changes,
3397 fine_grained=True,
3398 max_processes=options.maxjobs)
3399 proposal = [(cl.GetBranch(),
3400 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3401 for cl, status in statuses
3402 if status == 'closed']
3403 proposal.sort()
3404
3405 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003406 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003407 return 0
3408
3409 current_branch = GetCurrentBranch()
3410
vapiera7fbd5a2016-06-16 09:17:49 -07003411 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003412 if options.notags:
3413 for next_item in proposal:
3414 print(' ' + next_item[0])
3415 else:
3416 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3417 for next_item in proposal:
3418 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003419
kmarshall9249e012016-08-23 12:02:16 -07003420 # Quit now on precondition failure or if instructed by the user, either
3421 # via an interactive prompt or by command line flags.
3422 if options.dry_run:
3423 print('\nNo changes were made (dry run).\n')
3424 return 0
3425 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003426 print('You are currently on a branch \'%s\' which is associated with a '
3427 'closed codereview issue, so archive cannot proceed. Please '
3428 'checkout another branch and run this command again.' %
3429 current_branch)
3430 return 1
kmarshall9249e012016-08-23 12:02:16 -07003431 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003432 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3433 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003434 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003435 return 1
3436
3437 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003438 if not options.notags:
3439 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003440 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003441
vapiera7fbd5a2016-06-16 09:17:49 -07003442 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003443
3444 return 0
3445
3446
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003447def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003448 """Show status of changelists.
3449
3450 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003451 - Red not sent for review or broken
3452 - Blue waiting for review
3453 - Yellow waiting for you to reply to review
3454 - Green LGTM'ed
3455 - Magenta in the commit queue
3456 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003457
3458 Also see 'git cl comments'.
3459 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003460 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003461 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003462 parser.add_option('-f', '--fast', action='store_true',
3463 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003464 parser.add_option(
3465 '-j', '--maxjobs', action='store', type=int,
3466 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003467
3468 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003469 _add_codereview_issue_select_options(
3470 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003471 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003472 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003473 if args:
3474 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003475 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003476
iannuccie53c9352016-08-17 14:40:40 -07003477 if options.issue is not None and not options.field:
3478 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003479
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003480 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003481 cl = Changelist(auth_config=auth_config, issue=options.issue,
3482 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003483 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003484 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003485 elif options.field == 'id':
3486 issueid = cl.GetIssue()
3487 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003488 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003489 elif options.field == 'patch':
3490 patchset = cl.GetPatchset()
3491 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003492 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003493 elif options.field == 'status':
3494 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003495 elif options.field == 'url':
3496 url = cl.GetIssueURL()
3497 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003498 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003499 return 0
3500
3501 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3502 if not branches:
3503 print('No local branch found.')
3504 return 0
3505
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003506 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003507 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003508 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003509 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003510 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003511 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003512 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003513
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003514 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003515 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3516 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3517 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003518 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003519 c, status = output.next()
3520 branch_statuses[c.GetBranch()] = status
3521 status = branch_statuses.pop(branch)
3522 url = cl.GetIssueURL()
3523 if url and (not status or status == 'error'):
3524 # The issue probably doesn't exist anymore.
3525 url += ' (broken)'
3526
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003527 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003528 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003529 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003530 color = ''
3531 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003532 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003533 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003534 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003535 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003536
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003537 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003538 print()
3539 print('Current branch:',)
3540 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003541 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003542 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003543 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003544 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003545 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003546 print('Issue description:')
3547 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003548 return 0
3549
3550
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003551def colorize_CMDstatus_doc():
3552 """To be called once in main() to add colors to git cl status help."""
3553 colors = [i for i in dir(Fore) if i[0].isupper()]
3554
3555 def colorize_line(line):
3556 for color in colors:
3557 if color in line.upper():
3558 # Extract whitespaces first and the leading '-'.
3559 indent = len(line) - len(line.lstrip(' ')) + 1
3560 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3561 return line
3562
3563 lines = CMDstatus.__doc__.splitlines()
3564 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3565
3566
phajdan.jre328cf92016-08-22 04:12:17 -07003567def write_json(path, contents):
3568 with open(path, 'w') as f:
3569 json.dump(contents, f)
3570
3571
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003572@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003573def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003574 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003575
3576 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003577 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003578 parser.add_option('-r', '--reverse', action='store_true',
3579 help='Lookup the branch(es) for the specified issues. If '
3580 'no issues are specified, all branches with mapped '
3581 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003582 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003583 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003584 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003585 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003586
dnj@chromium.org406c4402015-03-03 17:22:28 +00003587 if options.reverse:
3588 branches = RunGit(['for-each-ref', 'refs/heads',
3589 '--format=%(refname:short)']).splitlines()
3590
3591 # Reverse issue lookup.
3592 issue_branch_map = {}
3593 for branch in branches:
3594 cl = Changelist(branchref=branch)
3595 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3596 if not args:
3597 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003598 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003599 for issue in args:
3600 if not issue:
3601 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003602 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003603 print('Branch for issue number %s: %s' % (
3604 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003605 if options.json:
3606 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003607 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003608 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003609 if len(args) > 0:
3610 try:
3611 issue = int(args[0])
3612 except ValueError:
3613 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003614 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003615 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003616 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003617 if options.json:
3618 write_json(options.json, {
3619 'issue': cl.GetIssue(),
3620 'issue_url': cl.GetIssueURL(),
3621 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003622 return 0
3623
3624
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003625def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003626 """Shows or posts review comments for any changelist."""
3627 parser.add_option('-a', '--add-comment', dest='comment',
3628 help='comment to add to an issue')
3629 parser.add_option('-i', dest='issue',
3630 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003631 parser.add_option('-j', '--json-file',
3632 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003633 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003634 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003635 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003636
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003637 issue = None
3638 if options.issue:
3639 try:
3640 issue = int(options.issue)
3641 except ValueError:
3642 DieWithError('A review issue id is expected to be a number')
3643
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003644 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003645
3646 if options.comment:
3647 cl.AddComment(options.comment)
3648 return 0
3649
3650 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003651 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003652 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003653 summary.append({
3654 'date': message['date'],
3655 'lgtm': False,
3656 'message': message['text'],
3657 'not_lgtm': False,
3658 'sender': message['sender'],
3659 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003660 if message['disapproval']:
3661 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003662 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003663 elif message['approval']:
3664 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003665 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003666 elif message['sender'] == data['owner_email']:
3667 color = Fore.MAGENTA
3668 else:
3669 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003670 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003671 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003672 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003673 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003674 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003675 if options.json_file:
3676 with open(options.json_file, 'wb') as f:
3677 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003678 return 0
3679
3680
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003681@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003682def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003683 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003684 parser.add_option('-d', '--display', action='store_true',
3685 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003686 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07003687 help='New description to set for this issue (- for stdin, '
3688 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07003689 parser.add_option('-f', '--force', action='store_true',
3690 help='Delete any unpublished Gerrit edits for this issue '
3691 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003692
3693 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003694 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003695 options, args = parser.parse_args(args)
3696 _process_codereview_select_options(parser, options)
3697
3698 target_issue = None
3699 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07003700 target_issue = ParseIssueNumberArgument(args[0])
3701 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003702 parser.print_help()
3703 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003704
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003705 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003706
martiniss6eda05f2016-06-30 10:18:35 -07003707 kwargs = {
3708 'auth_config': auth_config,
3709 'codereview': options.forced_codereview,
3710 }
3711 if target_issue:
3712 kwargs['issue'] = target_issue.issue
3713 if options.forced_codereview == 'rietveld':
3714 kwargs['rietveld_server'] = target_issue.hostname
3715
3716 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003717
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003718 if not cl.GetIssue():
3719 DieWithError('This branch has no associated changelist.')
3720 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003721
smut@google.com34fb6b12015-07-13 20:03:26 +00003722 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003723 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003724 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003725
3726 if options.new_description:
3727 text = options.new_description
3728 if text == '-':
3729 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07003730 elif text == '+':
3731 base_branch = cl.GetCommonAncestorWithUpstream()
3732 change = cl.GetChange(base_branch, None, local_description=True)
3733 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003734
3735 description.set_description(text)
3736 else:
3737 description.prompt()
3738
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003739 if cl.GetDescription() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07003740 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003741 return 0
3742
3743
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003744def CreateDescriptionFromLog(args):
3745 """Pulls out the commit log to use as a base for the CL description."""
3746 log_args = []
3747 if len(args) == 1 and not args[0].endswith('.'):
3748 log_args = [args[0] + '..']
3749 elif len(args) == 1 and args[0].endswith('...'):
3750 log_args = [args[0][:-1]]
3751 elif len(args) == 2:
3752 log_args = [args[0] + '..' + args[1]]
3753 else:
3754 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003755 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003756
3757
thestig@chromium.org44202a22014-03-11 19:22:18 +00003758def CMDlint(parser, args):
3759 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003760 parser.add_option('--filter', action='append', metavar='-x,+y',
3761 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003762 auth.add_auth_options(parser)
3763 options, args = parser.parse_args(args)
3764 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003765
3766 # Access to a protected member _XX of a client class
3767 # pylint: disable=W0212
3768 try:
3769 import cpplint
3770 import cpplint_chromium
3771 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003772 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003773 return 1
3774
3775 # Change the current working directory before calling lint so that it
3776 # shows the correct base.
3777 previous_cwd = os.getcwd()
3778 os.chdir(settings.GetRoot())
3779 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003780 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003781 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3782 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003783 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003784 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003785 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003786
3787 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003788 command = args + files
3789 if options.filter:
3790 command = ['--filter=' + ','.join(options.filter)] + command
3791 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003792
3793 white_regex = re.compile(settings.GetLintRegex())
3794 black_regex = re.compile(settings.GetLintIgnoreRegex())
3795 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3796 for filename in filenames:
3797 if white_regex.match(filename):
3798 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07003799 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003800 else:
3801 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3802 extra_check_functions)
3803 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003804 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003805 finally:
3806 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07003807 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003808 if cpplint._cpplint_state.error_count != 0:
3809 return 1
3810 return 0
3811
3812
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003813def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003814 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003815 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003816 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003817 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003818 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003819 auth.add_auth_options(parser)
3820 options, args = parser.parse_args(args)
3821 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003822
sbc@chromium.org71437c02015-04-09 19:29:40 +00003823 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07003824 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003825 return 1
3826
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003827 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003828 if args:
3829 base_branch = args[0]
3830 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003831 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003832 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003833
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003834 cl.RunHook(
3835 committing=not options.upload,
3836 may_prompt=False,
3837 verbose=options.verbose,
3838 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003839 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003840
3841
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003842def GenerateGerritChangeId(message):
3843 """Returns Ixxxxxx...xxx change id.
3844
3845 Works the same way as
3846 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3847 but can be called on demand on all platforms.
3848
3849 The basic idea is to generate git hash of a state of the tree, original commit
3850 message, author/committer info and timestamps.
3851 """
3852 lines = []
3853 tree_hash = RunGitSilent(['write-tree'])
3854 lines.append('tree %s' % tree_hash.strip())
3855 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3856 if code == 0:
3857 lines.append('parent %s' % parent.strip())
3858 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3859 lines.append('author %s' % author.strip())
3860 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3861 lines.append('committer %s' % committer.strip())
3862 lines.append('')
3863 # Note: Gerrit's commit-hook actually cleans message of some lines and
3864 # whitespace. This code is not doing this, but it clearly won't decrease
3865 # entropy.
3866 lines.append(message)
3867 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3868 stdin='\n'.join(lines))
3869 return 'I%s' % change_hash.strip()
3870
3871
wittman@chromium.org455dc922015-01-26 20:15:50 +00003872def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3873 """Computes the remote branch ref to use for the CL.
3874
3875 Args:
3876 remote (str): The git remote for the CL.
3877 remote_branch (str): The git remote branch for the CL.
3878 target_branch (str): The target branch specified by the user.
3879 pending_prefix (str): The pending prefix from the settings.
3880 """
3881 if not (remote and remote_branch):
3882 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003883
wittman@chromium.org455dc922015-01-26 20:15:50 +00003884 if target_branch:
3885 # Cannonicalize branch references to the equivalent local full symbolic
3886 # refs, which are then translated into the remote full symbolic refs
3887 # below.
3888 if '/' not in target_branch:
3889 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3890 else:
3891 prefix_replacements = (
3892 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3893 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3894 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3895 )
3896 match = None
3897 for regex, replacement in prefix_replacements:
3898 match = re.search(regex, target_branch)
3899 if match:
3900 remote_branch = target_branch.replace(match.group(0), replacement)
3901 break
3902 if not match:
3903 # This is a branch path but not one we recognize; use as-is.
3904 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003905 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3906 # Handle the refs that need to land in different refs.
3907 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003908
wittman@chromium.org455dc922015-01-26 20:15:50 +00003909 # Create the true path to the remote branch.
3910 # Does the following translation:
3911 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3912 # * refs/remotes/origin/master -> refs/heads/master
3913 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3914 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3915 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3916 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3917 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3918 'refs/heads/')
3919 elif remote_branch.startswith('refs/remotes/branch-heads'):
3920 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3921 # If a pending prefix exists then replace refs/ with it.
3922 if pending_prefix:
3923 remote_branch = remote_branch.replace('refs/', pending_prefix)
3924 return remote_branch
3925
3926
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003927def cleanup_list(l):
3928 """Fixes a list so that comma separated items are put as individual items.
3929
3930 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3931 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3932 """
3933 items = sum((i.split(',') for i in l), [])
3934 stripped_items = (i.strip() for i in items)
3935 return sorted(filter(None, stripped_items))
3936
3937
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003938@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003939def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003940 """Uploads the current changelist to codereview.
3941
3942 Can skip dependency patchset uploads for a branch by running:
3943 git config branch.branch_name.skip-deps-uploads True
3944 To unset run:
3945 git config --unset branch.branch_name.skip-deps-uploads
3946 Can also set the above globally by using the --global flag.
3947 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003948 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3949 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003950 parser.add_option('--bypass-watchlists', action='store_true',
3951 dest='bypass_watchlists',
3952 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003953 parser.add_option('-f', action='store_true', dest='force',
3954 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003955 parser.add_option('-m', dest='message', help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07003956 parser.add_option('-b', '--bug',
3957 help='pre-populate the bug number(s) for this issue. '
3958 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07003959 parser.add_option('--message-file', dest='message_file',
3960 help='file which contains message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003961 parser.add_option('-t', dest='title',
3962 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003963 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003964 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003965 help='reviewer email addresses')
3966 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003967 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003968 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003969 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003970 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003971 parser.add_option('--emulate_svn_auto_props',
3972 '--emulate-svn-auto-props',
3973 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003974 dest="emulate_svn_auto_props",
3975 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003976 parser.add_option('-c', '--use-commit-queue', action='store_true',
3977 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003978 parser.add_option('--private', action='store_true',
3979 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003980 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003981 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003982 metavar='TARGET',
3983 help='Apply CL to remote ref TARGET. ' +
3984 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003985 parser.add_option('--squash', action='store_true',
3986 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003987 parser.add_option('--no-squash', action='store_true',
3988 help='Don\'t squash multiple commits into one ' +
3989 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07003990 parser.add_option('--topic', default=None,
3991 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003992 parser.add_option('--email', default=None,
3993 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003994 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3995 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003996 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3997 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003998 help='Send the patchset to do a CQ dry run right after '
3999 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004000 parser.add_option('--dependencies', action='store_true',
4001 help='Uploads CLs of all the local branches that depend on '
4002 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004003
rmistry@google.com2dd99862015-06-22 12:22:18 +00004004 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004005 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004006 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004007 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004008 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004009 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004010 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004011
sbc@chromium.org71437c02015-04-09 19:29:40 +00004012 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004013 return 1
4014
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004015 options.reviewers = cleanup_list(options.reviewers)
4016 options.cc = cleanup_list(options.cc)
4017
tandriib80458a2016-06-23 12:20:07 -07004018 if options.message_file:
4019 if options.message:
4020 parser.error('only one of --message and --message-file allowed.')
4021 options.message = gclient_utils.FileRead(options.message_file)
4022 options.message_file = None
4023
tandrii4d0545a2016-07-06 03:56:49 -07004024 if options.cq_dry_run and options.use_commit_queue:
4025 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4026
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004027 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4028 settings.GetIsGerrit()
4029
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004030 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004031 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004032
4033
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004034def IsSubmoduleMergeCommit(ref):
4035 # When submodules are added to the repo, we expect there to be a single
4036 # non-git-svn merge commit at remote HEAD with a signature comment.
4037 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00004038 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004039 return RunGit(cmd) != ''
4040
4041
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004042def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004043 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004044
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004045 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4046 upstream and closes the issue automatically and atomically.
4047
4048 Otherwise (in case of Rietveld):
4049 Squashes branch into a single commit.
4050 Updates changelog with metadata (e.g. pointer to review).
4051 Pushes/dcommits the code upstream.
4052 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004053 """
4054 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4055 help='bypass upload presubmit hook')
4056 parser.add_option('-m', dest='message',
4057 help="override review description")
4058 parser.add_option('-f', action='store_true', dest='force',
4059 help="force yes to questions (don't prompt)")
4060 parser.add_option('-c', dest='contributor',
4061 help="external contributor for patch (appended to " +
4062 "description and used as author for git). Should be " +
4063 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004064 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004065 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004066 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004067 auth_config = auth.extract_auth_config_from_options(options)
4068
4069 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004070
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004071 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4072 if cl.IsGerrit():
4073 if options.message:
4074 # This could be implemented, but it requires sending a new patch to
4075 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4076 # Besides, Gerrit has the ability to change the commit message on submit
4077 # automatically, thus there is no need to support this option (so far?).
4078 parser.error('-m MESSAGE option is not supported for Gerrit.')
4079 if options.contributor:
4080 parser.error(
4081 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4082 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4083 'the contributor\'s "name <email>". If you can\'t upload such a '
4084 'commit for review, contact your repository admin and request'
4085 '"Forge-Author" permission.')
tandrii73449b02016-09-14 06:27:24 -07004086 if not cl.GetIssue():
4087 DieWithError('You must upload the issue first to Gerrit.\n'
4088 ' If you would rather have `git cl land` upload '
4089 'automatically for you, see http://crbug.com/642759')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004090 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4091 options.verbose)
4092
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004093 current = cl.GetBranch()
4094 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4095 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07004096 print()
4097 print('Attempting to push branch %r into another local branch!' % current)
4098 print()
4099 print('Either reparent this branch on top of origin/master:')
4100 print(' git reparent-branch --root')
4101 print()
4102 print('OR run `git rebase-update` if you think the parent branch is ')
4103 print('already committed.')
4104 print()
4105 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004106 return 1
4107
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004108 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004109 # Default to merging against our best guess of the upstream branch.
4110 args = [cl.GetUpstreamBranch()]
4111
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004112 if options.contributor:
4113 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07004114 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004115 return 1
4116
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004117 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004118 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004119
sbc@chromium.org71437c02015-04-09 19:29:40 +00004120 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004121 return 1
4122
4123 # This rev-list syntax means "show all commits not in my branch that
4124 # are in base_branch".
4125 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4126 base_branch]).splitlines()
4127 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004128 print('Base branch "%s" has %d commits '
4129 'not in this branch.' % (base_branch, len(upstream_commits)))
4130 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004131 return 1
4132
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004133 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004134 svn_head = None
4135 if cmd == 'dcommit' or base_has_submodules:
4136 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
4137 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004138
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004139 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004140 # If the base_head is a submodule merge commit, the first parent of the
4141 # base_head should be a git-svn commit, which is what we're interested in.
4142 base_svn_head = base_branch
4143 if base_has_submodules:
4144 base_svn_head += '^1'
4145
4146 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004147 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004148 print('This branch has %d additional commits not upstreamed yet.'
4149 % len(extra_commits.splitlines()))
4150 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
4151 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004152 return 1
4153
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004154 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004155 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004156 author = None
4157 if options.contributor:
4158 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004159 hook_results = cl.RunHook(
4160 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004161 may_prompt=not options.force,
4162 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004163 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004164 if not hook_results.should_continue():
4165 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004166
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004167 # Check the tree status if the tree status URL is set.
4168 status = GetTreeStatus()
4169 if 'closed' == status:
4170 print('The tree is closed. Please wait for it to reopen. Use '
4171 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4172 return 1
4173 elif 'unknown' == status:
4174 print('Unable to determine tree status. Please verify manually and '
4175 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4176 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004177
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004178 change_desc = ChangeDescription(options.message)
4179 if not change_desc.description and cl.GetIssue():
4180 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004181
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004182 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00004183 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004184 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00004185 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004186 print('No description set.')
4187 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00004188 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004189
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004190 # Keep a separate copy for the commit message, because the commit message
4191 # contains the link to the Rietveld issue, while the Rietveld message contains
4192 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004193 # Keep a separate copy for the commit message.
4194 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00004195 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004196
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004197 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00004198 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00004199 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00004200 # after it. Add a period on a new line to circumvent this. Also add a space
4201 # before the period to make sure that Gitiles continues to correctly resolve
4202 # the URL.
4203 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004204 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004205 commit_desc.append_footer('Patch from %s.' % options.contributor)
4206
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00004207 print('Description:')
4208 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004209
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004210 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004211 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00004212 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004213
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004214 # We want to squash all this branch's commits into one commit with the proper
4215 # description. We do this by doing a "reset --soft" to the base branch (which
4216 # keeps the working copy the same), then dcommitting that. If origin/master
4217 # has a submodule merge commit, we'll also need to cherry-pick the squashed
4218 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004219 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004220 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4221 # Delete the branches if they exist.
4222 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4223 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4224 result = RunGitWithCode(showref_cmd)
4225 if result[0] == 0:
4226 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004227
4228 # We might be in a directory that's present in this branch but not in the
4229 # trunk. Move up to the top of the tree so that git commands that expect a
4230 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004231 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004232 if rel_base_path:
4233 os.chdir(rel_base_path)
4234
4235 # Stuff our change into the merge branch.
4236 # We wrap in a try...finally block so if anything goes wrong,
4237 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004238 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004239 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004240 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004241 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004242 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00004243 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004244 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004245 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004246 RunGit(
4247 [
4248 'commit', '--author', options.contributor,
4249 '-m', commit_desc.description,
4250 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004251 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004252 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004253 if base_has_submodules:
4254 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
4255 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
4256 RunGit(['checkout', CHERRY_PICK_BRANCH])
4257 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004258 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004259 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004260 mirror = settings.GetGitMirror(remote)
4261 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004262 pending_prefix = settings.GetPendingRefPrefix()
4263 if not pending_prefix or branch.startswith(pending_prefix):
4264 # If not using refs/pending/heads/* at all, or target ref is already set
4265 # to pending, then push to the target ref directly.
4266 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004267 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004268 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004269 else:
4270 # Cherry-pick the change on top of pending ref and then push it.
4271 assert branch.startswith('refs/'), branch
4272 assert pending_prefix[-1] == '/', pending_prefix
4273 pending_ref = pending_prefix + branch[len('refs/'):]
tandriibf429402016-09-14 07:09:12 -07004274 retcode, output = PushToGitPending(pushurl, pending_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004275 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004276 if retcode == 0:
4277 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004278 else:
4279 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004280 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00004281 'svn', 'dcommit',
4282 '-C%s' % options.similarity,
4283 '--no-rebase', '--rmdir',
4284 ]
4285 if settings.GetForceHttpsCommitUrl():
4286 # Allow forcing https commit URLs for some projects that don't allow
4287 # committing to http URLs (like Google Code).
4288 remote_url = cl.GetGitSvnRemoteUrl()
4289 if urlparse.urlparse(remote_url).scheme == 'http':
4290 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004291 cmd_args.append('--commit-url=%s' % remote_url)
4292 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004293 if 'Committed r' in output:
4294 revision = re.match(
4295 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4296 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004297 finally:
4298 # And then swap back to the original branch and clean up.
4299 RunGit(['checkout', '-q', cl.GetBranch()])
4300 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004301 if base_has_submodules:
4302 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004303
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004304 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004305 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004306 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004307
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004308 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004309 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004310 try:
4311 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4312 # We set pushed_to_pending to False, since it made it all the way to the
4313 # real ref.
4314 pushed_to_pending = False
4315 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004316 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004317
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004318 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004319 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004320 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004321 if not to_pending:
4322 if viewvc_url and revision:
4323 change_desc.append_footer(
4324 'Committed: %s%s' % (viewvc_url, revision))
4325 elif revision:
4326 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004327 print('Closing issue '
4328 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004329 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004330 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004331 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004332 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004333 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004334 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004335 if options.bypass_hooks:
4336 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4337 else:
4338 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004339 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004340
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004341 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004342 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004343 print('The commit is in the pending queue (%s).' % pending_ref)
4344 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4345 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004346
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004347 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4348 if os.path.isfile(hook):
4349 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004350
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004351 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004352
4353
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004354def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004355 print()
4356 print('Waiting for commit to be landed on %s...' % real_ref)
4357 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004358 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4359 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004360 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004361
4362 loop = 0
4363 while True:
4364 sys.stdout.write('fetching (%d)... \r' % loop)
4365 sys.stdout.flush()
4366 loop += 1
4367
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004368 if mirror:
4369 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004370 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4371 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4372 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4373 for commit in commits.splitlines():
4374 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004375 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004376 return commit
4377
4378 current_rev = to_rev
4379
4380
tandriibf429402016-09-14 07:09:12 -07004381def PushToGitPending(remote, pending_ref):
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004382 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4383
4384 Returns:
4385 (retcode of last operation, output log of last operation).
4386 """
4387 assert pending_ref.startswith('refs/'), pending_ref
4388 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4389 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4390 code = 0
4391 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004392 max_attempts = 3
4393 attempts_left = max_attempts
4394 while attempts_left:
4395 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004396 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004397 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004398
4399 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004400 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004401 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004402 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004403 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004404 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004405 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004406 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004407 continue
4408
4409 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004410 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004411 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004412 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004413 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004414 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4415 'the following files have merge conflicts:' % pending_ref)
4416 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4417 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004418 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004419 return code, out
4420
4421 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004422 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004423 code, out = RunGitWithCode(
4424 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4425 if code == 0:
4426 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004427 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004428 return code, out
4429
vapiera7fbd5a2016-06-16 09:17:49 -07004430 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004431 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004432 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004433 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004434 print('Fatal push error. Make sure your .netrc credentials and git '
4435 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004436 return code, out
4437
vapiera7fbd5a2016-06-16 09:17:49 -07004438 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004439 return code, out
4440
4441
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004442def IsFatalPushFailure(push_stdout):
4443 """True if retrying push won't help."""
4444 return '(prohibited by Gerrit)' in push_stdout
4445
4446
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004447@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004448def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004449 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004450 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004451 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004452 # If it looks like previous commits were mirrored with git-svn.
agable3b9a5bb2016-09-22 11:32:08 -07004453 message = """This repository appears to be a git-svn mirror, but we
4454don't support git-svn mirrors anymore."""
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004455 else:
4456 message = """This doesn't appear to be an SVN repository.
4457If your project has a true, writeable git repository, you probably want to run
4458'git cl land' instead.
4459If your project has a git mirror of an upstream SVN master, you probably need
4460to run 'git svn init'.
4461
4462Using the wrong command might cause your commit to appear to succeed, and the
4463review to be closed, without actually landing upstream. If you choose to
4464proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004465 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004466 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
tandrii3bb82ff2016-06-17 07:36:36 -07004467 print('WARNING: chrome infrastructure is migrating SVN repos to Git.\n'
4468 'Please let us know of this project you are committing to:'
4469 ' http://crbug.com/600451')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004470 return SendUpstream(parser, args, 'dcommit')
4471
4472
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004473@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004474def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004475 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004476 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004477 print('This appears to be an SVN repository.')
4478 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004479 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004480 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004481 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004482
4483
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004484@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004485def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004486 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004487 parser.add_option('-b', dest='newbranch',
4488 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004489 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004490 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004491 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4492 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004493 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004494 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004495 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004496 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004497 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004498 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004499
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004500
4501 group = optparse.OptionGroup(
4502 parser,
4503 'Options for continuing work on the current issue uploaded from a '
4504 'different clone (e.g. different machine). Must be used independently '
4505 'from the other options. No issue number should be specified, and the '
4506 'branch must have an issue number associated with it')
4507 group.add_option('--reapply', action='store_true', dest='reapply',
4508 help='Reset the branch and reapply the issue.\n'
4509 'CAUTION: This will undo any local changes in this '
4510 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004511
4512 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004513 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004514 parser.add_option_group(group)
4515
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004516 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004517 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004518 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004519 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004520 auth_config = auth.extract_auth_config_from_options(options)
4521
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004522
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004523 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004524 if options.newbranch:
4525 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004526 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004527 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004528
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004529 cl = Changelist(auth_config=auth_config,
4530 codereview=options.forced_codereview)
4531 if not cl.GetIssue():
4532 parser.error('current branch must have an associated issue')
4533
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004534 upstream = cl.GetUpstreamBranch()
4535 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004536 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004537
4538 RunGit(['reset', '--hard', upstream])
4539 if options.pull:
4540 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004541
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004542 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4543 options.directory)
4544
4545 if len(args) != 1 or not args[0]:
4546 parser.error('Must specify issue number or url')
4547
4548 # We don't want uncommitted changes mixed up with the patch.
4549 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004550 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004551
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004552 if options.newbranch:
4553 if options.force:
4554 RunGit(['branch', '-D', options.newbranch],
4555 stderr=subprocess2.PIPE, error_ok=True)
4556 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004557 elif not GetCurrentBranch():
4558 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004559
4560 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4561
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004562 if cl.IsGerrit():
4563 if options.reject:
4564 parser.error('--reject is not supported with Gerrit codereview.')
4565 if options.nocommit:
4566 parser.error('--nocommit is not supported with Gerrit codereview.')
4567 if options.directory:
4568 parser.error('--directory is not supported with Gerrit codereview.')
4569
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004570 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004571 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004572
4573
4574def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004575 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004576 # Provide a wrapper for git svn rebase to help avoid accidental
4577 # git svn dcommit.
4578 # It's the only command that doesn't use parser at all since we just defer
4579 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004580
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004581 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004582
4583
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004584def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004585 """Fetches the tree status and returns either 'open', 'closed',
4586 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004587 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004588 if url:
4589 status = urllib2.urlopen(url).read().lower()
4590 if status.find('closed') != -1 or status == '0':
4591 return 'closed'
4592 elif status.find('open') != -1 or status == '1':
4593 return 'open'
4594 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004595 return 'unset'
4596
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004597
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004598def GetTreeStatusReason():
4599 """Fetches the tree status from a json url and returns the message
4600 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004601 url = settings.GetTreeStatusUrl()
4602 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004603 connection = urllib2.urlopen(json_url)
4604 status = json.loads(connection.read())
4605 connection.close()
4606 return status['message']
4607
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004608
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004609def GetBuilderMaster(bot_list):
4610 """For a given builder, fetch the master from AE if available."""
4611 map_url = 'https://builders-map.appspot.com/'
4612 try:
4613 master_map = json.load(urllib2.urlopen(map_url))
4614 except urllib2.URLError as e:
4615 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4616 (map_url, e))
4617 except ValueError as e:
4618 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4619 if not master_map:
4620 return None, 'Failed to build master map.'
4621
4622 result_master = ''
4623 for bot in bot_list:
4624 builder = bot.split(':', 1)[0]
4625 master_list = master_map.get(builder, [])
4626 if not master_list:
4627 return None, ('No matching master for builder %s.' % builder)
4628 elif len(master_list) > 1:
4629 return None, ('The builder name %s exists in multiple masters %s.' %
4630 (builder, master_list))
4631 else:
4632 cur_master = master_list[0]
4633 if not result_master:
4634 result_master = cur_master
4635 elif result_master != cur_master:
4636 return None, 'The builders do not belong to the same master.'
4637 return result_master, None
4638
4639
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004640def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004641 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004642 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004643 status = GetTreeStatus()
4644 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004645 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004646 return 2
4647
vapiera7fbd5a2016-06-16 09:17:49 -07004648 print('The tree is %s' % status)
4649 print()
4650 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004651 if status != 'open':
4652 return 1
4653 return 0
4654
4655
maruel@chromium.org15192402012-09-06 12:38:29 +00004656def CMDtry(parser, args):
tandriif7b29d42016-10-07 08:45:41 -07004657 """Triggers try jobs using CQ dry run or BuildBucket for individual builders.
4658 """
tandrii1838bad2016-10-06 00:10:52 -07004659 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004660 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004661 '-b', '--bot', action='append',
4662 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4663 'times to specify multiple builders. ex: '
4664 '"-b win_rel -b win_layout". See '
4665 'the try server waterfall for the builders name and the tests '
4666 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004667 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004668 '-m', '--master', default='',
4669 help=('Specify a try master where to run the tries.'))
tandriif7b29d42016-10-07 08:45:41 -07004670 # TODO(tandrii,nodir): add -B --bucket flag.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004671 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004672 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07004673 help='Revision to use for the try job; default: the revision will '
4674 'be determined by the try recipe that builder runs, which usually '
4675 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004676 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004677 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004678 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004679 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004680 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004681 '--project',
4682 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004683 'in recipe to determine to which repository or directory to '
4684 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004685 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004686 '-p', '--property', dest='properties', action='append', default=[],
4687 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004688 'key2=value2 etc. The value will be treated as '
4689 'json if decodable, or as string otherwise. '
4690 'NOTE: using this may make your try job not usable for CQ, '
4691 'which will then schedule another try job with default properties')
4692 # TODO(tandrii): if this even used?
machenbach@chromium.org45453142015-09-15 08:45:22 +00004693 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004694 '-n', '--name', help='Try job name; default to current branch name')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004695 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004696 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4697 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004698 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004699 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004700 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004701 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004702
machenbach@chromium.org45453142015-09-15 08:45:22 +00004703 # Make sure that all properties are prop=value pairs.
4704 bad_params = [x for x in options.properties if '=' not in x]
4705 if bad_params:
4706 parser.error('Got properties with missing "=": %s' % bad_params)
4707
maruel@chromium.org15192402012-09-06 12:38:29 +00004708 if args:
4709 parser.error('Unknown arguments: %s' % args)
4710
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004711 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004712 if not cl.GetIssue():
4713 parser.error('Need to upload first')
4714
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004715 if cl.IsGerrit():
4716 parser.error(
4717 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4718 'If your project has Commit Queue, dry run is a workaround:\n'
4719 ' git cl set-commit --dry-run')
4720 # Code below assumes Rietveld issue.
4721 # TODO(tandrii): actually implement for Gerrit http://crbug.com/599931.
4722
tandriie113dfd2016-10-11 10:20:12 -07004723 error_message = cl.CannotTriggerTryJobReason()
4724 if error_message:
4725 parser.error('Can\'t trigger try jobs: %s')
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004726
maruel@chromium.org15192402012-09-06 12:38:29 +00004727 if not options.name:
4728 options.name = cl.GetBranch()
4729
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004730 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004731 options.master, err_msg = GetBuilderMaster(options.bot)
4732 if err_msg:
4733 parser.error('Tryserver master cannot be found because: %s\n'
4734 'Please manually specify the tryserver master'
4735 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004736
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004737 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004738 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004739 if not options.bot:
4740 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004741
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004742 # Get try masters from PRESUBMIT.py files.
4743 masters = presubmit_support.DoGetTryMasters(
4744 change,
4745 change.LocalPaths(),
4746 settings.GetRoot(),
4747 None,
4748 None,
4749 options.verbose,
4750 sys.stdout)
4751 if masters:
4752 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004753
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004754 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4755 options.bot = presubmit_support.DoGetTrySlaves(
4756 change,
4757 change.LocalPaths(),
4758 settings.GetRoot(),
4759 None,
4760 None,
4761 options.verbose,
4762 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004763
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004764 if not options.bot:
tandrii9de9ec62016-07-13 03:01:59 -07004765 return {}
maruel@chromium.org15192402012-09-06 12:38:29 +00004766
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004767 builders_and_tests = {}
4768 # TODO(machenbach): The old style command-line options don't support
4769 # multiple try masters yet.
4770 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4771 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4772
4773 for bot in old_style:
4774 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004775 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004776 elif ',' in bot:
4777 parser.error('Specify one bot per --bot flag')
4778 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004779 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004780
4781 for bot, tests in new_style:
4782 builders_and_tests.setdefault(bot, []).extend(tests)
4783
4784 # Return a master map with one master to be backwards compatible. The
4785 # master name defaults to an empty string, which will cause the master
4786 # not to be set on rietveld (deprecated).
4787 return {options.master: builders_and_tests}
4788
4789 masters = GetMasterMap()
tandrii9de9ec62016-07-13 03:01:59 -07004790 if not masters:
4791 # Default to triggering Dry Run (see http://crbug.com/625697).
4792 if options.verbose:
4793 print('git cl try with no bots now defaults to CQ Dry Run.')
4794 try:
4795 cl.SetCQState(_CQState.DRY_RUN)
4796 print('scheduled CQ Dry Run on %s' % cl.GetIssueURL())
4797 return 0
4798 except KeyboardInterrupt:
4799 raise
4800 except:
4801 print('WARNING: failed to trigger CQ Dry Run.\n'
4802 'Either:\n'
4803 ' * your project has no CQ\n'
4804 ' * you don\'t have permission to trigger Dry Run\n'
4805 ' * bug in this code (see stack trace below).\n'
4806 'Consider specifying which bots to trigger manually '
4807 'or asking your project owners for permissions '
4808 'or contacting Chrome Infrastructure team at '
4809 'https://www.chromium.org/infra\n\n')
4810 # Still raise exception so that stack trace is printed.
4811 raise
stip@chromium.org43064fd2013-12-18 20:07:44 +00004812
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004813 for builders in masters.itervalues():
4814 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004815 print('ERROR You are trying to send a job to a triggered bot. This type '
4816 'of bot requires an\ninitial job from a parent (usually a builder).'
4817 ' Instead send your job to the parent.\n'
4818 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004819 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004820
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004821 patchset = cl.GetMostRecentPatchset()
4822 if patchset and patchset != cl.GetPatchset():
4823 print(
4824 '\nWARNING Mismatch between local config and server. Did a previous '
4825 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4826 'Continuing using\npatchset %s.\n' % patchset)
tandrii568043b2016-10-11 07:49:18 -07004827 try:
4828 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4829 except BuildbucketResponseException as ex:
4830 print('ERROR: %s' % ex)
4831 return 1
4832 except Exception as e:
4833 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4834 print('ERROR: Exception when trying to trigger try jobs: %s\n%s' %
4835 (e, stacktrace))
4836 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00004837 return 0
4838
4839
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004840def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07004841 """Prints info about try jobs associated with current CL."""
4842 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004843 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004844 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004845 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004846 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004847 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004848 '--color', action='store_true', default=setup_color.IS_TTY,
4849 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004850 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004851 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4852 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004853 group.add_option(
4854 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004855 parser.add_option_group(group)
4856 auth.add_auth_options(parser)
4857 options, args = parser.parse_args(args)
4858 if args:
4859 parser.error('Unrecognized args: %s' % ' '.join(args))
4860
4861 auth_config = auth.extract_auth_config_from_options(options)
4862 cl = Changelist(auth_config=auth_config)
4863 if not cl.GetIssue():
4864 parser.error('Need to upload first')
4865
tandrii221ab252016-10-06 08:12:04 -07004866 patchset = options.patchset
4867 if not patchset:
4868 patchset = cl.GetMostRecentPatchset()
4869 if not patchset:
4870 parser.error('Codereview doesn\'t know about issue %s. '
4871 'No access to issue or wrong issue number?\n'
4872 'Either upload first, or pass --patchset explicitely' %
4873 cl.GetIssue())
4874
4875 if patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07004876 print('Warning: Codereview server has newer patchsets (%s) than most '
4877 'recent upload from local checkout (%s). Did a previous upload '
4878 'fail?\n'
4879 'By default, git cl try uses latest patchset from codereview, '
4880 'continuing to use patchset %s.\n' %
4881 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004882 try:
tandrii221ab252016-10-06 08:12:04 -07004883 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004884 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004885 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004886 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004887 if options.json:
4888 write_try_results_json(options.json, jobs)
4889 else:
4890 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004891 return 0
4892
4893
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004894@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004895def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004896 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004897 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004898 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004899 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004900
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004901 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004902 if args:
4903 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004904 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07004905 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004906 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004907 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004908
4909 # Clear configured merge-base, if there is one.
4910 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004911 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004912 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004913 return 0
4914
4915
thestig@chromium.org00858c82013-12-02 23:08:03 +00004916def CMDweb(parser, args):
4917 """Opens the current CL in the web browser."""
4918 _, args = parser.parse_args(args)
4919 if args:
4920 parser.error('Unrecognized args: %s' % ' '.join(args))
4921
4922 issue_url = Changelist().GetIssueURL()
4923 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004924 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004925 return 1
4926
4927 webbrowser.open(issue_url)
4928 return 0
4929
4930
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004931def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004932 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004933 parser.add_option('-d', '--dry-run', action='store_true',
4934 help='trigger in dry run mode')
4935 parser.add_option('-c', '--clear', action='store_true',
4936 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004937 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004938 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004939 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004940 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004941 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004942 if args:
4943 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004944 if options.dry_run and options.clear:
4945 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4946
iannuccie53c9352016-08-17 14:40:40 -07004947 cl = Changelist(auth_config=auth_config, issue=options.issue,
4948 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004949 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07004950 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004951 elif options.dry_run:
4952 state = _CQState.DRY_RUN
4953 else:
4954 state = _CQState.COMMIT
4955 if not cl.GetIssue():
4956 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07004957 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004958 return 0
4959
4960
groby@chromium.org411034a2013-02-26 15:12:01 +00004961def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004962 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07004963 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004964 auth.add_auth_options(parser)
4965 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004966 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004967 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004968 if args:
4969 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07004970 cl = Changelist(auth_config=auth_config, issue=options.issue,
4971 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00004972 # Ensure there actually is an issue to close.
4973 cl.GetDescription()
4974 cl.CloseIssue()
4975 return 0
4976
4977
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004978def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004979 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07004980 parser.add_option(
4981 '--stat',
4982 action='store_true',
4983 dest='stat',
4984 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004985 auth.add_auth_options(parser)
4986 options, args = parser.parse_args(args)
4987 auth_config = auth.extract_auth_config_from_options(options)
4988 if args:
4989 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004990
4991 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004992 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004993 # Staged changes would be committed along with the patch from last
4994 # upload, hence counted toward the "last upload" side in the final
4995 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004996 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004997 return 1
4998
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004999 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005000 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005001 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005002 if not issue:
5003 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005004 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005005 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005006
5007 # Create a new branch based on the merge-base
5008 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00005009 # Clear cached branch in cl object, to avoid overwriting original CL branch
5010 # properties.
5011 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005012 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005013 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005014 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00005015 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005016 return rtn
5017
wychen@chromium.org06928532015-02-03 02:11:29 +00005018 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005019 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07005020 cmd = ['git', 'diff']
5021 if options.stat:
5022 cmd.append('--stat')
5023 cmd.extend([TMP_BRANCH, branch, '--'])
5024 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005025 finally:
5026 RunGit(['checkout', '-q', branch])
5027 RunGit(['branch', '-D', TMP_BRANCH])
5028
5029 return 0
5030
5031
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005032def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005033 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005034 parser.add_option(
5035 '--no-color',
5036 action='store_true',
5037 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005038 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005039 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005040 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005041
5042 author = RunGit(['config', 'user.email']).strip() or None
5043
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005044 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005045
5046 if args:
5047 if len(args) > 1:
5048 parser.error('Unknown args')
5049 base_branch = args[0]
5050 else:
5051 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005052 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005053
5054 change = cl.GetChange(base_branch, None)
5055 return owners_finder.OwnersFinder(
5056 [f.LocalPath() for f in
5057 cl.GetChange(base_branch, None).AffectedFiles()],
5058 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07005059 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005060 disable_color=options.no_color).run()
5061
5062
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005063def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005064 """Generates a diff command."""
5065 # Generate diff for the current branch's changes.
5066 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
5067 upstream_commit, '--' ]
5068
5069 if args:
5070 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005071 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005072 diff_cmd.append(arg)
5073 else:
5074 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005075
5076 return diff_cmd
5077
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005078def MatchingFileType(file_name, extensions):
5079 """Returns true if the file name ends with one of the given extensions."""
5080 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005081
enne@chromium.org555cfe42014-01-29 18:21:39 +00005082@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005083def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005084 """Runs auto-formatting tools (clang-format etc.) on the diff."""
zengsterbf470142016-07-07 16:43:00 -07005085 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005086 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005087 parser.add_option('--full', action='store_true',
5088 help='Reformat the full content of all touched files')
5089 parser.add_option('--dry-run', action='store_true',
5090 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005091 parser.add_option('--python', action='store_true',
5092 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005093 parser.add_option('--diff', action='store_true',
5094 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005095 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005096
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005097 # git diff generates paths against the root of the repository. Change
5098 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005099 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005100 if rel_base_path:
5101 os.chdir(rel_base_path)
5102
digit@chromium.org29e47272013-05-17 17:01:46 +00005103 # Grab the merge-base commit, i.e. the upstream commit of the current
5104 # branch when it was created or the last time it was rebased. This is
5105 # to cover the case where the user may have called "git fetch origin",
5106 # moving the origin branch to a newer commit, but hasn't rebased yet.
5107 upstream_commit = None
5108 cl = Changelist()
5109 upstream_branch = cl.GetUpstreamBranch()
5110 if upstream_branch:
5111 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5112 upstream_commit = upstream_commit.strip()
5113
5114 if not upstream_commit:
5115 DieWithError('Could not find base commit for this branch. '
5116 'Are you in detached state?')
5117
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005118 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5119 diff_output = RunGit(changed_files_cmd)
5120 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005121 # Filter out files deleted by this CL
5122 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005123
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005124 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5125 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5126 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005127 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005128
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005129 top_dir = os.path.normpath(
5130 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5131
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005132 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5133 # formatted. This is used to block during the presubmit.
5134 return_value = 0
5135
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005136 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005137 # Locate the clang-format binary in the checkout
5138 try:
5139 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005140 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005141 DieWithError(e)
5142
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005143 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005144 cmd = [clang_format_tool]
5145 if not opts.dry_run and not opts.diff:
5146 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005147 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005148 if opts.diff:
5149 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005150 else:
5151 env = os.environ.copy()
5152 env['PATH'] = str(os.path.dirname(clang_format_tool))
5153 try:
5154 script = clang_format.FindClangFormatScriptInChromiumTree(
5155 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005156 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005157 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005158
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005159 cmd = [sys.executable, script, '-p0']
5160 if not opts.dry_run and not opts.diff:
5161 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005162
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005163 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5164 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005165
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005166 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5167 if opts.diff:
5168 sys.stdout.write(stdout)
5169 if opts.dry_run and len(stdout) > 0:
5170 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005171
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005172 # Similar code to above, but using yapf on .py files rather than clang-format
5173 # on C/C++ files
5174 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005175 yapf_tool = gclient_utils.FindExecutable('yapf')
5176 if yapf_tool is None:
5177 DieWithError('yapf not found in PATH')
5178
5179 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005180 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005181 cmd = [yapf_tool]
5182 if not opts.dry_run and not opts.diff:
5183 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005184 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005185 if opts.diff:
5186 sys.stdout.write(stdout)
5187 else:
5188 # TODO(sbc): yapf --lines mode still has some issues.
5189 # https://github.com/google/yapf/issues/154
5190 DieWithError('--python currently only works with --full')
5191
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005192 # Dart's formatter does not have the nice property of only operating on
5193 # modified chunks, so hard code full.
5194 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005195 try:
5196 command = [dart_format.FindDartFmtToolInChromiumTree()]
5197 if not opts.dry_run and not opts.diff:
5198 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005199 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005200
ppi@chromium.org6593d932016-03-03 15:41:15 +00005201 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005202 if opts.dry_run and stdout:
5203 return_value = 2
5204 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005205 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5206 'found in this checkout. Files in other languages are still '
5207 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005208
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005209 # Format GN build files. Always run on full build files for canonical form.
5210 if gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005211 cmd = ['gn', 'format' ]
5212 if opts.dry_run or opts.diff:
5213 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005214 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005215 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5216 shell=sys.platform == 'win32',
5217 cwd=top_dir)
5218 if opts.dry_run and gn_ret == 2:
5219 return_value = 2 # Not formatted.
5220 elif opts.diff and gn_ret == 2:
5221 # TODO this should compute and print the actual diff.
5222 print("This change has GN build file diff for " + gn_diff_file)
5223 elif gn_ret != 0:
5224 # For non-dry run cases (and non-2 return values for dry-run), a
5225 # nonzero error code indicates a failure, probably because the file
5226 # doesn't parse.
5227 DieWithError("gn format failed on " + gn_diff_file +
5228 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005229
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005230 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005231
5232
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005233@subcommand.usage('<codereview url or issue id>')
5234def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005235 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005236 _, args = parser.parse_args(args)
5237
5238 if len(args) != 1:
5239 parser.print_help()
5240 return 1
5241
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005242 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005243 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005244 parser.print_help()
5245 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005246 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005247
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005248 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005249 output = RunGit(['config', '--local', '--get-regexp',
5250 r'branch\..*\.%s' % issueprefix],
5251 error_ok=True)
5252 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005253 if issue == target_issue:
5254 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005255
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005256 branches = []
5257 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005258 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005259 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005260 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005261 return 1
5262 if len(branches) == 1:
5263 RunGit(['checkout', branches[0]])
5264 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005265 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005266 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005267 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005268 which = raw_input('Choose by index: ')
5269 try:
5270 RunGit(['checkout', branches[int(which)]])
5271 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005272 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005273 return 1
5274
5275 return 0
5276
5277
maruel@chromium.org29404b52014-09-08 22:58:00 +00005278def CMDlol(parser, args):
5279 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005280 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005281 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5282 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5283 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005284 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005285 return 0
5286
5287
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005288class OptionParser(optparse.OptionParser):
5289 """Creates the option parse and add --verbose support."""
5290 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005291 optparse.OptionParser.__init__(
5292 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005293 self.add_option(
5294 '-v', '--verbose', action='count', default=0,
5295 help='Use 2 times for more debugging info')
5296
5297 def parse_args(self, args=None, values=None):
5298 options, args = optparse.OptionParser.parse_args(self, args, values)
5299 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5300 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5301 return options, args
5302
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005303
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005304def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005305 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005306 print('\nYour python version %s is unsupported, please upgrade.\n' %
5307 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005308 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005309
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005310 # Reload settings.
5311 global settings
5312 settings = Settings()
5313
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005314 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005315 dispatcher = subcommand.CommandDispatcher(__name__)
5316 try:
5317 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005318 except auth.AuthenticationError as e:
5319 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005320 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005321 if e.code != 500:
5322 raise
5323 DieWithError(
5324 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5325 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005326 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005327
5328
5329if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005330 # These affect sys.stdout so do it outside of main() to simplify mocks in
5331 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005332 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005333 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005334 try:
5335 sys.exit(main(sys.argv[1:]))
5336 except KeyboardInterrupt:
5337 sys.stderr.write('interrupted\n')
5338 sys.exit(1)