blob: c2675730cc4fc9c92de43750eca680381a6384bd [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
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000398def fetch_try_jobs(auth_config, changelist, options):
qyearsleyeab3c042016-08-24 09:18:28 -0700399 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000400
qyearsley53f48a12016-09-01 10:45:13 -0700401 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000402 """
403 rietveld_url = settings.GetDefaultServerUrl()
404 rietveld_host = urlparse.urlparse(rietveld_url).hostname
405 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
406 if authenticator.has_cached_credentials():
407 http = authenticator.authorize(httplib2.Http())
408 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700409 print('Warning: Some results might be missing because %s' %
410 # Get the message on how to login.
411 (auth.LoginRequiredError(rietveld_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000412 http = httplib2.Http()
413
414 http.force_exception_to_status_code = True
415
416 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
417 hostname=rietveld_host,
418 issue=changelist.GetIssue(),
419 patch=options.patchset)
420 params = {'tag': 'buildset:%s' % buildset}
421
422 builds = {}
423 while True:
424 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
425 hostname=options.buildbucket_host,
426 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700427 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000428 for build in content.get('builds', []):
429 builds[build['id']] = build
430 if 'next_cursor' in content:
431 params['start_cursor'] = content['next_cursor']
432 else:
433 break
434 return builds
435
436
qyearsleyeab3c042016-08-24 09:18:28 -0700437def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000438 """Prints nicely result of fetch_try_jobs."""
439 if not builds:
qyearsleyeab3c042016-08-24 09:18:28 -0700440 print('No try jobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000441 return
442
443 # Make a copy, because we'll be modifying builds dictionary.
444 builds = builds.copy()
445 builder_names_cache = {}
446
447 def get_builder(b):
448 try:
449 return builder_names_cache[b['id']]
450 except KeyError:
451 try:
452 parameters = json.loads(b['parameters_json'])
453 name = parameters['builder_name']
454 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700455 print('WARNING: failed to get builder name for build %s: %s' % (
456 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000457 name = None
458 builder_names_cache[b['id']] = name
459 return name
460
461 def get_bucket(b):
462 bucket = b['bucket']
463 if bucket.startswith('master.'):
464 return bucket[len('master.'):]
465 return bucket
466
467 if options.print_master:
468 name_fmt = '%%-%ds %%-%ds' % (
469 max(len(str(get_bucket(b))) for b in builds.itervalues()),
470 max(len(str(get_builder(b))) for b in builds.itervalues()))
471 def get_name(b):
472 return name_fmt % (get_bucket(b), get_builder(b))
473 else:
474 name_fmt = '%%-%ds' % (
475 max(len(str(get_builder(b))) for b in builds.itervalues()))
476 def get_name(b):
477 return name_fmt % get_builder(b)
478
479 def sort_key(b):
480 return b['status'], b.get('result'), get_name(b), b.get('url')
481
482 def pop(title, f, color=None, **kwargs):
483 """Pop matching builds from `builds` dict and print them."""
484
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000485 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000486 colorize = str
487 else:
488 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
489
490 result = []
491 for b in builds.values():
492 if all(b.get(k) == v for k, v in kwargs.iteritems()):
493 builds.pop(b['id'])
494 result.append(b)
495 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700496 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000497 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700498 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000499
500 total = len(builds)
501 pop(status='COMPLETED', result='SUCCESS',
502 title='Successes:', color=Fore.GREEN,
503 f=lambda b: (get_name(b), b.get('url')))
504 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
505 title='Infra Failures:', color=Fore.MAGENTA,
506 f=lambda b: (get_name(b), b.get('url')))
507 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
508 title='Failures:', color=Fore.RED,
509 f=lambda b: (get_name(b), b.get('url')))
510 pop(status='COMPLETED', result='CANCELED',
511 title='Canceled:', color=Fore.MAGENTA,
512 f=lambda b: (get_name(b),))
513 pop(status='COMPLETED', result='FAILURE',
514 failure_reason='INVALID_BUILD_DEFINITION',
515 title='Wrong master/builder name:', color=Fore.MAGENTA,
516 f=lambda b: (get_name(b),))
517 pop(status='COMPLETED', result='FAILURE',
518 title='Other failures:',
519 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
520 pop(status='COMPLETED',
521 title='Other finished:',
522 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
523 pop(status='STARTED',
524 title='Started:', color=Fore.YELLOW,
525 f=lambda b: (get_name(b), b.get('url')))
526 pop(status='SCHEDULED',
527 title='Scheduled:',
528 f=lambda b: (get_name(b), 'id=%s' % b['id']))
529 # The last section is just in case buildbucket API changes OR there is a bug.
530 pop(title='Other:',
531 f=lambda b: (get_name(b), 'id=%s' % b['id']))
532 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700533 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000534
535
qyearsley53f48a12016-09-01 10:45:13 -0700536def write_try_results_json(output_file, builds):
537 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
538
539 The input |builds| dict is assumed to be generated by Buildbucket.
540 Buildbucket documentation: http://goo.gl/G0s101
541 """
542
543 def convert_build_dict(build):
544 return {
545 'buildbucket_id': build.get('id'),
546 'status': build.get('status'),
547 'result': build.get('result'),
548 'bucket': build.get('bucket'),
549 'builder_name': json.loads(
550 build.get('parameters_json', '{}')).get('builder_name'),
551 'failure_reason': build.get('failure_reason'),
552 'url': build.get('url'),
553 }
554
555 converted = []
556 for _, build in sorted(builds.items()):
557 converted.append(convert_build_dict(build))
558 write_json(output_file, converted)
559
560
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000561def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
562 """Return the corresponding git ref if |base_url| together with |glob_spec|
563 matches the full |url|.
564
565 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
566 """
567 fetch_suburl, as_ref = glob_spec.split(':')
568 if allow_wildcards:
569 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
570 if glob_match:
571 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
572 # "branches/{472,597,648}/src:refs/remotes/svn/*".
573 branch_re = re.escape(base_url)
574 if glob_match.group(1):
575 branch_re += '/' + re.escape(glob_match.group(1))
576 wildcard = glob_match.group(2)
577 if wildcard == '*':
578 branch_re += '([^/]*)'
579 else:
580 # Escape and replace surrounding braces with parentheses and commas
581 # with pipe symbols.
582 wildcard = re.escape(wildcard)
583 wildcard = re.sub('^\\\\{', '(', wildcard)
584 wildcard = re.sub('\\\\,', '|', wildcard)
585 wildcard = re.sub('\\\\}$', ')', wildcard)
586 branch_re += wildcard
587 if glob_match.group(3):
588 branch_re += re.escape(glob_match.group(3))
589 match = re.match(branch_re, url)
590 if match:
591 return re.sub('\*$', match.group(1), as_ref)
592
593 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
594 if fetch_suburl:
595 full_url = base_url + '/' + fetch_suburl
596 else:
597 full_url = base_url
598 if full_url == url:
599 return as_ref
600 return None
601
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000602
iannucci@chromium.org79540052012-10-19 23:15:26 +0000603def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000604 """Prints statistics about the change to the user."""
605 # --no-ext-diff is broken in some versions of Git, so try to work around
606 # this by overriding the environment (but there is still a problem if the
607 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000608 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000609 if 'GIT_EXTERNAL_DIFF' in env:
610 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000611
612 if find_copies:
613 similarity_options = ['--find-copies-harder', '-l100000',
614 '-C%s' % similarity]
615 else:
616 similarity_options = ['-M%s' % similarity]
617
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000618 try:
619 stdout = sys.stdout.fileno()
620 except AttributeError:
621 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000622 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000623 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000624 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000625 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000626
627
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000628class BuildbucketResponseException(Exception):
629 pass
630
631
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000632class Settings(object):
633 def __init__(self):
634 self.default_server = None
635 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000636 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000637 self.is_git_svn = None
638 self.svn_branch = None
639 self.tree_status_url = None
640 self.viewvc_url = None
641 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000642 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000643 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000644 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000645 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000646 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000647 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000648 self.pending_ref_prefix = None
tandriif46c20f2016-09-14 06:17:05 -0700649 self.git_number_footer = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000650
651 def LazyUpdateIfNeeded(self):
652 """Updates the settings from a codereview.settings file, if available."""
653 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000654 # The only value that actually changes the behavior is
655 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000656 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000657 error_ok=True
658 ).strip().lower()
659
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000660 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000661 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000662 LoadCodereviewSettingsFromFile(cr_settings_file)
663 self.updated = True
664
665 def GetDefaultServerUrl(self, error_ok=False):
666 if not self.default_server:
667 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000668 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000669 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000670 if error_ok:
671 return self.default_server
672 if not self.default_server:
673 error_message = ('Could not find settings file. You must configure '
674 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000675 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000676 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000677 return self.default_server
678
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000679 @staticmethod
680 def GetRelativeRoot():
681 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000682
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000683 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000684 if self.root is None:
685 self.root = os.path.abspath(self.GetRelativeRoot())
686 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000687
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000688 def GetGitMirror(self, remote='origin'):
689 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000690 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000691 if not os.path.isdir(local_url):
692 return None
693 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
694 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
695 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
696 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
697 if mirror.exists():
698 return mirror
699 return None
700
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000701 def GetIsGitSvn(self):
702 """Return true if this repo looks like it's using git-svn."""
703 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000704 if self.GetPendingRefPrefix():
705 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
706 self.is_git_svn = False
707 else:
708 # If you have any "svn-remote.*" config keys, we think you're using svn.
709 self.is_git_svn = RunGitWithCode(
710 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000711 return self.is_git_svn
712
713 def GetSVNBranch(self):
714 if self.svn_branch is None:
715 if not self.GetIsGitSvn():
716 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
717
718 # Try to figure out which remote branch we're based on.
719 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000720 # 1) iterate through our branch history and find the svn URL.
721 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000722
723 # regexp matching the git-svn line that contains the URL.
724 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
725
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000726 # We don't want to go through all of history, so read a line from the
727 # pipe at a time.
728 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000729 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000730 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
731 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000732 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000733 for line in proc.stdout:
734 match = git_svn_re.match(line)
735 if match:
736 url = match.group(1)
737 proc.stdout.close() # Cut pipe.
738 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000739
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000740 if url:
741 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
742 remotes = RunGit(['config', '--get-regexp',
743 r'^svn-remote\..*\.url']).splitlines()
744 for remote in remotes:
745 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000746 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000747 remote = match.group(1)
748 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000749 rewrite_root = RunGit(
750 ['config', 'svn-remote.%s.rewriteRoot' % remote],
751 error_ok=True).strip()
752 if rewrite_root:
753 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000754 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000755 ['config', 'svn-remote.%s.fetch' % remote],
756 error_ok=True).strip()
757 if fetch_spec:
758 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
759 if self.svn_branch:
760 break
761 branch_spec = RunGit(
762 ['config', 'svn-remote.%s.branches' % remote],
763 error_ok=True).strip()
764 if branch_spec:
765 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
766 if self.svn_branch:
767 break
768 tag_spec = RunGit(
769 ['config', 'svn-remote.%s.tags' % remote],
770 error_ok=True).strip()
771 if tag_spec:
772 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
773 if self.svn_branch:
774 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000775
776 if not self.svn_branch:
777 DieWithError('Can\'t guess svn branch -- try specifying it on the '
778 'command line')
779
780 return self.svn_branch
781
782 def GetTreeStatusUrl(self, error_ok=False):
783 if not self.tree_status_url:
784 error_message = ('You must configure your tree status URL by running '
785 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000786 self.tree_status_url = self._GetRietveldConfig(
787 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000788 return self.tree_status_url
789
790 def GetViewVCUrl(self):
791 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000792 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000793 return self.viewvc_url
794
rmistry@google.com90752582014-01-14 21:04:50 +0000795 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000796 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000797
rmistry@google.com78948ed2015-07-08 23:09:57 +0000798 def GetIsSkipDependencyUpload(self, branch_name):
799 """Returns true if specified branch should skip dep uploads."""
800 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
801 error_ok=True)
802
rmistry@google.com5626a922015-02-26 14:03:30 +0000803 def GetRunPostUploadHook(self):
804 run_post_upload_hook = self._GetRietveldConfig(
805 'run-post-upload-hook', error_ok=True)
806 return run_post_upload_hook == "True"
807
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000808 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000809 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000810
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000811 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000812 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000813
ukai@chromium.orge8077812012-02-03 03:41:46 +0000814 def GetIsGerrit(self):
815 """Return true if this repo is assosiated with gerrit code review system."""
816 if self.is_gerrit is None:
817 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
818 return self.is_gerrit
819
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000820 def GetSquashGerritUploads(self):
821 """Return true if uploads to Gerrit should be squashed by default."""
822 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700823 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
824 if self.squash_gerrit_uploads is None:
825 # Default is squash now (http://crbug.com/611892#c23).
826 self.squash_gerrit_uploads = not (
827 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
828 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000829 return self.squash_gerrit_uploads
830
tandriia60502f2016-06-20 02:01:53 -0700831 def GetSquashGerritUploadsOverride(self):
832 """Return True or False if codereview.settings should be overridden.
833
834 Returns None if no override has been defined.
835 """
836 # See also http://crbug.com/611892#c23
837 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
838 error_ok=True).strip()
839 if result == 'true':
840 return True
841 if result == 'false':
842 return False
843 return None
844
tandrii@chromium.org28253532016-04-14 13:46:56 +0000845 def GetGerritSkipEnsureAuthenticated(self):
846 """Return True if EnsureAuthenticated should not be done for Gerrit
847 uploads."""
848 if self.gerrit_skip_ensure_authenticated is None:
849 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000850 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000851 error_ok=True).strip() == 'true')
852 return self.gerrit_skip_ensure_authenticated
853
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000854 def GetGitEditor(self):
855 """Return the editor specified in the git config, or None if none is."""
856 if self.git_editor is None:
857 self.git_editor = self._GetConfig('core.editor', error_ok=True)
858 return self.git_editor or None
859
thestig@chromium.org44202a22014-03-11 19:22:18 +0000860 def GetLintRegex(self):
861 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
862 DEFAULT_LINT_REGEX)
863
864 def GetLintIgnoreRegex(self):
865 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
866 DEFAULT_LINT_IGNORE_REGEX)
867
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000868 def GetProject(self):
869 if not self.project:
870 self.project = self._GetRietveldConfig('project', error_ok=True)
871 return self.project
872
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000873 def GetForceHttpsCommitUrl(self):
874 if not self.force_https_commit_url:
875 self.force_https_commit_url = self._GetRietveldConfig(
876 'force-https-commit-url', error_ok=True)
877 return self.force_https_commit_url
878
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000879 def GetPendingRefPrefix(self):
880 if not self.pending_ref_prefix:
881 self.pending_ref_prefix = self._GetRietveldConfig(
882 'pending-ref-prefix', error_ok=True)
883 return self.pending_ref_prefix
884
tandriif46c20f2016-09-14 06:17:05 -0700885 def GetHasGitNumberFooter(self):
886 # TODO(tandrii): this has to be removed after Rietveld is read-only.
887 # see also bugs http://crbug.com/642493 and http://crbug.com/600469.
888 if not self.git_number_footer:
889 self.git_number_footer = self._GetRietveldConfig(
890 'git-number-footer', error_ok=True)
891 return self.git_number_footer
892
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000893 def _GetRietveldConfig(self, param, **kwargs):
894 return self._GetConfig('rietveld.' + param, **kwargs)
895
rmistry@google.com78948ed2015-07-08 23:09:57 +0000896 def _GetBranchConfig(self, branch_name, param, **kwargs):
897 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
898
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000899 def _GetConfig(self, param, **kwargs):
900 self.LazyUpdateIfNeeded()
901 return RunGit(['config', param], **kwargs).strip()
902
903
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000904def ShortBranchName(branch):
905 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000906 return branch.replace('refs/heads/', '', 1)
907
908
909def GetCurrentBranchRef():
910 """Returns branch ref (e.g., refs/heads/master) or None."""
911 return RunGit(['symbolic-ref', 'HEAD'],
912 stderr=subprocess2.VOID, error_ok=True).strip() or None
913
914
915def GetCurrentBranch():
916 """Returns current branch or None.
917
918 For refs/heads/* branches, returns just last part. For others, full ref.
919 """
920 branchref = GetCurrentBranchRef()
921 if branchref:
922 return ShortBranchName(branchref)
923 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000924
925
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000926class _CQState(object):
927 """Enum for states of CL with respect to Commit Queue."""
928 NONE = 'none'
929 DRY_RUN = 'dry_run'
930 COMMIT = 'commit'
931
932 ALL_STATES = [NONE, DRY_RUN, COMMIT]
933
934
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000935class _ParsedIssueNumberArgument(object):
936 def __init__(self, issue=None, patchset=None, hostname=None):
937 self.issue = issue
938 self.patchset = patchset
939 self.hostname = hostname
940
941 @property
942 def valid(self):
943 return self.issue is not None
944
945
946class _RietveldParsedIssueNumberArgument(_ParsedIssueNumberArgument):
947 def __init__(self, *args, **kwargs):
948 self.patch_url = kwargs.pop('patch_url', None)
949 super(_RietveldParsedIssueNumberArgument, self).__init__(*args, **kwargs)
950
951
952def ParseIssueNumberArgument(arg):
953 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
954 fail_result = _ParsedIssueNumberArgument()
955
956 if arg.isdigit():
957 return _ParsedIssueNumberArgument(issue=int(arg))
958 if not arg.startswith('http'):
959 return fail_result
960 url = gclient_utils.UpgradeToHttps(arg)
961 try:
962 parsed_url = urlparse.urlparse(url)
963 except ValueError:
964 return fail_result
965 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
966 tmp = cls.ParseIssueURL(parsed_url)
967 if tmp is not None:
968 return tmp
969 return fail_result
970
971
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000972class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000973 """Changelist works with one changelist in local branch.
974
975 Supports two codereview backends: Rietveld or Gerrit, selected at object
976 creation.
977
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +0000978 Notes:
979 * Not safe for concurrent multi-{thread,process} use.
980 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -0700981 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000982 """
983
984 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
985 """Create a new ChangeList instance.
986
987 If issue is given, the codereview must be given too.
988
989 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
990 Otherwise, it's decided based on current configuration of the local branch,
991 with default being 'rietveld' for backwards compatibility.
992 See _load_codereview_impl for more details.
993
994 **kwargs will be passed directly to codereview implementation.
995 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000996 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000997 global settings
998 if not settings:
999 # Happens when git_cl.py is used as a utility library.
1000 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001001
1002 if issue:
1003 assert codereview, 'codereview must be known, if issue is known'
1004
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001005 self.branchref = branchref
1006 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001007 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001008 self.branch = ShortBranchName(self.branchref)
1009 else:
1010 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001011 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001012 self.lookedup_issue = False
1013 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001014 self.has_description = False
1015 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001016 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001017 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001018 self.cc = None
1019 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001020 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001021
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001022 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001023 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001024 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001025 assert self._codereview_impl
1026 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001027
1028 def _load_codereview_impl(self, codereview=None, **kwargs):
1029 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001030 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1031 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1032 self._codereview = codereview
1033 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001034 return
1035
1036 # Automatic selection based on issue number set for a current branch.
1037 # Rietveld takes precedence over Gerrit.
1038 assert not self.issue
1039 # Whether we find issue or not, we are doing the lookup.
1040 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001041 if self.GetBranch():
1042 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1043 issue = _git_get_branch_config_value(
1044 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1045 if issue:
1046 self._codereview = codereview
1047 self._codereview_impl = cls(self, **kwargs)
1048 self.issue = int(issue)
1049 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001050
1051 # No issue is set for this branch, so decide based on repo-wide settings.
1052 return self._load_codereview_impl(
1053 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1054 **kwargs)
1055
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001056 def IsGerrit(self):
1057 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001058
1059 def GetCCList(self):
1060 """Return the users cc'd on this CL.
1061
agable92bec4f2016-08-24 09:27:27 -07001062 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001063 """
1064 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001065 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001066 more_cc = ','.join(self.watchers)
1067 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1068 return self.cc
1069
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001070 def GetCCListWithoutDefault(self):
1071 """Return the users cc'd on this CL excluding default ones."""
1072 if self.cc is None:
1073 self.cc = ','.join(self.watchers)
1074 return self.cc
1075
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001076 def SetWatchers(self, watchers):
1077 """Set the list of email addresses that should be cc'd based on the changed
1078 files in this CL.
1079 """
1080 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001081
1082 def GetBranch(self):
1083 """Returns the short branch name, e.g. 'master'."""
1084 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001085 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001086 if not branchref:
1087 return None
1088 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001089 self.branch = ShortBranchName(self.branchref)
1090 return self.branch
1091
1092 def GetBranchRef(self):
1093 """Returns the full branch name, e.g. 'refs/heads/master'."""
1094 self.GetBranch() # Poke the lazy loader.
1095 return self.branchref
1096
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001097 def ClearBranch(self):
1098 """Clears cached branch data of this object."""
1099 self.branch = self.branchref = None
1100
tandrii5d48c322016-08-18 16:19:37 -07001101 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1102 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1103 kwargs['branch'] = self.GetBranch()
1104 return _git_get_branch_config_value(key, default, **kwargs)
1105
1106 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1107 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1108 assert self.GetBranch(), (
1109 'this CL must have an associated branch to %sset %s%s' %
1110 ('un' if value is None else '',
1111 key,
1112 '' if value is None else ' to %r' % value))
1113 kwargs['branch'] = self.GetBranch()
1114 return _git_set_branch_config_value(key, value, **kwargs)
1115
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001116 @staticmethod
1117 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001118 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001119 e.g. 'origin', 'refs/heads/master'
1120 """
1121 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001122 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1123
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001124 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001125 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001126 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001127 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1128 error_ok=True).strip()
1129 if upstream_branch:
1130 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001131 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001132 # Fall back on trying a git-svn upstream branch.
1133 if settings.GetIsGitSvn():
1134 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001135 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001136 # Else, try to guess the origin remote.
1137 remote_branches = RunGit(['branch', '-r']).split()
1138 if 'origin/master' in remote_branches:
1139 # Fall back on origin/master if it exits.
1140 remote = 'origin'
1141 upstream_branch = 'refs/heads/master'
1142 elif 'origin/trunk' in remote_branches:
1143 # Fall back on origin/trunk if it exists. Generally a shared
1144 # git-svn clone
1145 remote = 'origin'
1146 upstream_branch = 'refs/heads/trunk'
1147 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001148 DieWithError(
1149 'Unable to determine default branch to diff against.\n'
1150 'Either pass complete "git diff"-style arguments, like\n'
1151 ' git cl upload origin/master\n'
1152 'or verify this branch is set up to track another \n'
1153 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001154
1155 return remote, upstream_branch
1156
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001157 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001158 upstream_branch = self.GetUpstreamBranch()
1159 if not BranchExists(upstream_branch):
1160 DieWithError('The upstream for the current branch (%s) does not exist '
1161 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001162 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001163 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001164
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001165 def GetUpstreamBranch(self):
1166 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001167 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001168 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001169 upstream_branch = upstream_branch.replace('refs/heads/',
1170 'refs/remotes/%s/' % remote)
1171 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1172 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001173 self.upstream_branch = upstream_branch
1174 return self.upstream_branch
1175
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001176 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001177 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001178 remote, branch = None, self.GetBranch()
1179 seen_branches = set()
1180 while branch not in seen_branches:
1181 seen_branches.add(branch)
1182 remote, branch = self.FetchUpstreamTuple(branch)
1183 branch = ShortBranchName(branch)
1184 if remote != '.' or branch.startswith('refs/remotes'):
1185 break
1186 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001187 remotes = RunGit(['remote'], error_ok=True).split()
1188 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001189 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001190 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001191 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001192 logging.warning('Could not determine which remote this change is '
1193 'associated with, so defaulting to "%s". This may '
1194 'not be what you want. You may prevent this message '
1195 'by running "git svn info" as documented here: %s',
1196 self._remote,
1197 GIT_INSTRUCTIONS_URL)
1198 else:
1199 logging.warn('Could not determine which remote this change is '
1200 'associated with. You may prevent this message by '
1201 'running "git svn info" as documented here: %s',
1202 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001203 branch = 'HEAD'
1204 if branch.startswith('refs/remotes'):
1205 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001206 elif branch.startswith('refs/branch-heads/'):
1207 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001208 else:
1209 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001210 return self._remote
1211
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001212 def GitSanityChecks(self, upstream_git_obj):
1213 """Checks git repo status and ensures diff is from local commits."""
1214
sbc@chromium.org79706062015-01-14 21:18:12 +00001215 if upstream_git_obj is None:
1216 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001217 print('ERROR: unable to determine current branch (detached HEAD?)',
1218 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001219 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001220 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001221 return False
1222
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001223 # Verify the commit we're diffing against is in our current branch.
1224 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1225 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1226 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001227 print('ERROR: %s is not in the current branch. You may need to rebase '
1228 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001229 return False
1230
1231 # List the commits inside the diff, and verify they are all local.
1232 commits_in_diff = RunGit(
1233 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1234 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1235 remote_branch = remote_branch.strip()
1236 if code != 0:
1237 _, remote_branch = self.GetRemoteBranch()
1238
1239 commits_in_remote = RunGit(
1240 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1241
1242 common_commits = set(commits_in_diff) & set(commits_in_remote)
1243 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001244 print('ERROR: Your diff contains %d commits already in %s.\n'
1245 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1246 'the diff. If you are using a custom git flow, you can override'
1247 ' the reference used for this check with "git config '
1248 'gitcl.remotebranch <git-ref>".' % (
1249 len(common_commits), remote_branch, upstream_git_obj),
1250 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001251 return False
1252 return True
1253
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001254 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001255 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001256
1257 Returns None if it is not set.
1258 """
tandrii5d48c322016-08-18 16:19:37 -07001259 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001260
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001261 def GetGitSvnRemoteUrl(self):
1262 """Return the configured git-svn remote URL parsed from git svn info.
1263
1264 Returns None if it is not set.
1265 """
1266 # URL is dependent on the current directory.
1267 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1268 if data:
1269 keys = dict(line.split(': ', 1) for line in data.splitlines()
1270 if ': ' in line)
1271 return keys.get('URL', None)
1272 return None
1273
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001274 def GetRemoteUrl(self):
1275 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1276
1277 Returns None if there is no remote.
1278 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001279 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001280 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1281
1282 # If URL is pointing to a local directory, it is probably a git cache.
1283 if os.path.isdir(url):
1284 url = RunGit(['config', 'remote.%s.url' % remote],
1285 error_ok=True,
1286 cwd=url).strip()
1287 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001288
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001289 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001290 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001291 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001292 self.issue = self._GitGetBranchConfigValue(
1293 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001294 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001295 return self.issue
1296
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001297 def GetIssueURL(self):
1298 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001299 issue = self.GetIssue()
1300 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001301 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001302 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001303
1304 def GetDescription(self, pretty=False):
1305 if not self.has_description:
1306 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001307 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001308 self.has_description = True
1309 if pretty:
1310 wrapper = textwrap.TextWrapper()
1311 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1312 return wrapper.fill(self.description)
1313 return self.description
1314
1315 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001316 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001317 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001318 self.patchset = self._GitGetBranchConfigValue(
1319 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001320 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001321 return self.patchset
1322
1323 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001324 """Set this branch's patchset. If patchset=0, clears the patchset."""
1325 assert self.GetBranch()
1326 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001327 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001328 else:
1329 self.patchset = int(patchset)
1330 self._GitSetBranchConfigValue(
1331 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001332
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001333 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001334 """Set this branch's issue. If issue isn't given, clears the issue."""
1335 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001336 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001337 issue = int(issue)
1338 self._GitSetBranchConfigValue(
1339 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001340 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001341 codereview_server = self._codereview_impl.GetCodereviewServer()
1342 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001343 self._GitSetBranchConfigValue(
1344 self._codereview_impl.CodereviewServerConfigKey(),
1345 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001346 else:
tandrii5d48c322016-08-18 16:19:37 -07001347 # Reset all of these just to be clean.
1348 reset_suffixes = [
1349 'last-upload-hash',
1350 self._codereview_impl.IssueConfigKey(),
1351 self._codereview_impl.PatchsetConfigKey(),
1352 self._codereview_impl.CodereviewServerConfigKey(),
1353 ] + self._PostUnsetIssueProperties()
1354 for prop in reset_suffixes:
1355 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001356 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001357 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001358
dnjba1b0f32016-09-02 12:37:42 -07001359 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001360 if not self.GitSanityChecks(upstream_branch):
1361 DieWithError('\nGit sanity check failure')
1362
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001363 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001364 if not root:
1365 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001366 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001367
1368 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001369 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001370 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001371 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001372 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001373 except subprocess2.CalledProcessError:
1374 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001375 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001376 'This branch probably doesn\'t exist anymore. To reset the\n'
1377 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001378 ' git branch --set-upstream-to origin/master %s\n'
1379 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001380 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001381
maruel@chromium.org52424302012-08-29 15:14:30 +00001382 issue = self.GetIssue()
1383 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001384 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001385 description = self.GetDescription()
1386 else:
1387 # If the change was never uploaded, use the log messages of all commits
1388 # up to the branch point, as git cl upload will prefill the description
1389 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001390 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1391 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001392
1393 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001394 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001395 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001396 name,
1397 description,
1398 absroot,
1399 files,
1400 issue,
1401 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001402 author,
1403 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001404
dsansomee2d6fd92016-09-08 00:10:47 -07001405 def UpdateDescription(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001406 self.description = description
dsansomee2d6fd92016-09-08 00:10:47 -07001407 return self._codereview_impl.UpdateDescriptionRemote(
1408 description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001409
1410 def RunHook(self, committing, may_prompt, verbose, change):
1411 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1412 try:
1413 return presubmit_support.DoPresubmitChecks(change, committing,
1414 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1415 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001416 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1417 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001418 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001419 DieWithError(
1420 ('%s\nMaybe your depot_tools is out of date?\n'
1421 'If all fails, contact maruel@') % e)
1422
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001423 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1424 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001425 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1426 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001427 else:
1428 # Assume url.
1429 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1430 urlparse.urlparse(issue_arg))
1431 if not parsed_issue_arg or not parsed_issue_arg.valid:
1432 DieWithError('Failed to parse issue argument "%s". '
1433 'Must be an issue number or a valid URL.' % issue_arg)
1434 return self._codereview_impl.CMDPatchWithParsedIssue(
1435 parsed_issue_arg, reject, nocommit, directory)
1436
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001437 def CMDUpload(self, options, git_diff_args, orig_args):
1438 """Uploads a change to codereview."""
1439 if git_diff_args:
1440 # TODO(ukai): is it ok for gerrit case?
1441 base_branch = git_diff_args[0]
1442 else:
1443 if self.GetBranch() is None:
1444 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1445
1446 # Default to diffing against common ancestor of upstream branch
1447 base_branch = self.GetCommonAncestorWithUpstream()
1448 git_diff_args = [base_branch, 'HEAD']
1449
1450 # Make sure authenticated to codereview before running potentially expensive
1451 # hooks. It is a fast, best efforts check. Codereview still can reject the
1452 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001453 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001454
1455 # Apply watchlists on upload.
1456 change = self.GetChange(base_branch, None)
1457 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1458 files = [f.LocalPath() for f in change.AffectedFiles()]
1459 if not options.bypass_watchlists:
1460 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1461
1462 if not options.bypass_hooks:
1463 if options.reviewers or options.tbr_owners:
1464 # Set the reviewer list now so that presubmit checks can access it.
1465 change_description = ChangeDescription(change.FullDescriptionText())
1466 change_description.update_reviewers(options.reviewers,
1467 options.tbr_owners,
1468 change)
1469 change.SetDescriptionText(change_description.description)
1470 hook_results = self.RunHook(committing=False,
1471 may_prompt=not options.force,
1472 verbose=options.verbose,
1473 change=change)
1474 if not hook_results.should_continue():
1475 return 1
1476 if not options.reviewers and hook_results.reviewers:
1477 options.reviewers = hook_results.reviewers.split(',')
1478
1479 if self.GetIssue():
1480 latest_patchset = self.GetMostRecentPatchset()
1481 local_patchset = self.GetPatchset()
1482 if (latest_patchset and local_patchset and
1483 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001484 print('The last upload made from this repository was patchset #%d but '
1485 'the most recent patchset on the server is #%d.'
1486 % (local_patchset, latest_patchset))
1487 print('Uploading will still work, but if you\'ve uploaded to this '
1488 'issue from another machine or branch the patch you\'re '
1489 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001490 ask_for_data('About to upload; enter to confirm.')
1491
1492 print_stats(options.similarity, options.find_copies, git_diff_args)
1493 ret = self.CMDUploadChange(options, git_diff_args, change)
1494 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001495 if options.use_commit_queue:
1496 self.SetCQState(_CQState.COMMIT)
1497 elif options.cq_dry_run:
1498 self.SetCQState(_CQState.DRY_RUN)
1499
tandrii5d48c322016-08-18 16:19:37 -07001500 _git_set_branch_config_value('last-upload-hash',
1501 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001502 # Run post upload hooks, if specified.
1503 if settings.GetRunPostUploadHook():
1504 presubmit_support.DoPostUploadExecuter(
1505 change,
1506 self,
1507 settings.GetRoot(),
1508 options.verbose,
1509 sys.stdout)
1510
1511 # Upload all dependencies if specified.
1512 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001513 print()
1514 print('--dependencies has been specified.')
1515 print('All dependent local branches will be re-uploaded.')
1516 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001517 # Remove the dependencies flag from args so that we do not end up in a
1518 # loop.
1519 orig_args.remove('--dependencies')
1520 ret = upload_branch_deps(self, orig_args)
1521 return ret
1522
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001523 def SetCQState(self, new_state):
1524 """Update the CQ state for latest patchset.
1525
1526 Issue must have been already uploaded and known.
1527 """
1528 assert new_state in _CQState.ALL_STATES
1529 assert self.GetIssue()
1530 return self._codereview_impl.SetCQState(new_state)
1531
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001532 # Forward methods to codereview specific implementation.
1533
1534 def CloseIssue(self):
1535 return self._codereview_impl.CloseIssue()
1536
1537 def GetStatus(self):
1538 return self._codereview_impl.GetStatus()
1539
1540 def GetCodereviewServer(self):
1541 return self._codereview_impl.GetCodereviewServer()
1542
1543 def GetApprovingReviewers(self):
1544 return self._codereview_impl.GetApprovingReviewers()
1545
1546 def GetMostRecentPatchset(self):
1547 return self._codereview_impl.GetMostRecentPatchset()
1548
1549 def __getattr__(self, attr):
1550 # This is because lots of untested code accesses Rietveld-specific stuff
1551 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001552 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001553 # Note that child method defines __getattr__ as well, and forwards it here,
1554 # because _RietveldChangelistImpl is not cleaned up yet, and given
1555 # deprecation of Rietveld, it should probably be just removed.
1556 # Until that time, avoid infinite recursion by bypassing __getattr__
1557 # of implementation class.
1558 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001559
1560
1561class _ChangelistCodereviewBase(object):
1562 """Abstract base class encapsulating codereview specifics of a changelist."""
1563 def __init__(self, changelist):
1564 self._changelist = changelist # instance of Changelist
1565
1566 def __getattr__(self, attr):
1567 # Forward methods to changelist.
1568 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1569 # _RietveldChangelistImpl to avoid this hack?
1570 return getattr(self._changelist, attr)
1571
1572 def GetStatus(self):
1573 """Apply a rough heuristic to give a simple summary of an issue's review
1574 or CQ status, assuming adherence to a common workflow.
1575
1576 Returns None if no issue for this branch, or specific string keywords.
1577 """
1578 raise NotImplementedError()
1579
1580 def GetCodereviewServer(self):
1581 """Returns server URL without end slash, like "https://codereview.com"."""
1582 raise NotImplementedError()
1583
1584 def FetchDescription(self):
1585 """Fetches and returns description from the codereview server."""
1586 raise NotImplementedError()
1587
tandrii5d48c322016-08-18 16:19:37 -07001588 @classmethod
1589 def IssueConfigKey(cls):
1590 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001591 raise NotImplementedError()
1592
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001593 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001594 def PatchsetConfigKey(cls):
1595 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001596 raise NotImplementedError()
1597
tandrii5d48c322016-08-18 16:19:37 -07001598 @classmethod
1599 def CodereviewServerConfigKey(cls):
1600 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001601 raise NotImplementedError()
1602
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001603 def _PostUnsetIssueProperties(self):
1604 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001605 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001606
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001607 def GetRieveldObjForPresubmit(self):
1608 # This is an unfortunate Rietveld-embeddedness in presubmit.
1609 # For non-Rietveld codereviews, this probably should return a dummy object.
1610 raise NotImplementedError()
1611
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001612 def GetGerritObjForPresubmit(self):
1613 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1614 return None
1615
dsansomee2d6fd92016-09-08 00:10:47 -07001616 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001617 """Update the description on codereview site."""
1618 raise NotImplementedError()
1619
1620 def CloseIssue(self):
1621 """Closes the issue."""
1622 raise NotImplementedError()
1623
1624 def GetApprovingReviewers(self):
1625 """Returns a list of reviewers approving the change.
1626
1627 Note: not necessarily committers.
1628 """
1629 raise NotImplementedError()
1630
1631 def GetMostRecentPatchset(self):
1632 """Returns the most recent patchset number from the codereview site."""
1633 raise NotImplementedError()
1634
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001635 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1636 directory):
1637 """Fetches and applies the issue.
1638
1639 Arguments:
1640 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1641 reject: if True, reject the failed patch instead of switching to 3-way
1642 merge. Rietveld only.
1643 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1644 only.
1645 directory: switch to directory before applying the patch. Rietveld only.
1646 """
1647 raise NotImplementedError()
1648
1649 @staticmethod
1650 def ParseIssueURL(parsed_url):
1651 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1652 failed."""
1653 raise NotImplementedError()
1654
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001655 def EnsureAuthenticated(self, force):
1656 """Best effort check that user is authenticated with codereview server.
1657
1658 Arguments:
1659 force: whether to skip confirmation questions.
1660 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001661 raise NotImplementedError()
1662
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001663 def CMDUploadChange(self, options, args, change):
1664 """Uploads a change to codereview."""
1665 raise NotImplementedError()
1666
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001667 def SetCQState(self, new_state):
1668 """Update the CQ state for latest patchset.
1669
1670 Issue must have been already uploaded and known.
1671 """
1672 raise NotImplementedError()
1673
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001674
1675class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1676 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1677 super(_RietveldChangelistImpl, self).__init__(changelist)
1678 assert settings, 'must be initialized in _ChangelistCodereviewBase'
martiniss6eda05f2016-06-30 10:18:35 -07001679 if not rietveld_server:
1680 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001681
1682 self._rietveld_server = rietveld_server
1683 self._auth_config = auth_config
1684 self._props = None
1685 self._rpc_server = None
1686
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001687 def GetCodereviewServer(self):
1688 if not self._rietveld_server:
1689 # If we're on a branch then get the server potentially associated
1690 # with that branch.
1691 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001692 self._rietveld_server = gclient_utils.UpgradeToHttps(
1693 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001694 if not self._rietveld_server:
1695 self._rietveld_server = settings.GetDefaultServerUrl()
1696 return self._rietveld_server
1697
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001698 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001699 """Best effort check that user is authenticated with Rietveld server."""
1700 if self._auth_config.use_oauth2:
1701 authenticator = auth.get_authenticator_for_host(
1702 self.GetCodereviewServer(), self._auth_config)
1703 if not authenticator.has_cached_credentials():
1704 raise auth.LoginRequiredError(self.GetCodereviewServer())
1705
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001706 def FetchDescription(self):
1707 issue = self.GetIssue()
1708 assert issue
1709 try:
1710 return self.RpcServer().get_description(issue).strip()
1711 except urllib2.HTTPError as e:
1712 if e.code == 404:
1713 DieWithError(
1714 ('\nWhile fetching the description for issue %d, received a '
1715 '404 (not found)\n'
1716 'error. It is likely that you deleted this '
1717 'issue on the server. If this is the\n'
1718 'case, please run\n\n'
1719 ' git cl issue 0\n\n'
1720 'to clear the association with the deleted issue. Then run '
1721 'this command again.') % issue)
1722 else:
1723 DieWithError(
1724 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1725 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001726 print('Warning: Failed to retrieve CL description due to network '
1727 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001728 return ''
1729
1730 def GetMostRecentPatchset(self):
1731 return self.GetIssueProperties()['patchsets'][-1]
1732
1733 def GetPatchSetDiff(self, issue, patchset):
1734 return self.RpcServer().get(
1735 '/download/issue%s_%s.diff' % (issue, patchset))
1736
1737 def GetIssueProperties(self):
1738 if self._props is None:
1739 issue = self.GetIssue()
1740 if not issue:
1741 self._props = {}
1742 else:
1743 self._props = self.RpcServer().get_issue_properties(issue, True)
1744 return self._props
1745
1746 def GetApprovingReviewers(self):
1747 return get_approving_reviewers(self.GetIssueProperties())
1748
1749 def AddComment(self, message):
1750 return self.RpcServer().add_comment(self.GetIssue(), message)
1751
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001752 def GetStatus(self):
1753 """Apply a rough heuristic to give a simple summary of an issue's review
1754 or CQ status, assuming adherence to a common workflow.
1755
1756 Returns None if no issue for this branch, or one of the following keywords:
1757 * 'error' - error from review tool (including deleted issues)
1758 * 'unsent' - not sent for review
1759 * 'waiting' - waiting for review
1760 * 'reply' - waiting for owner to reply to review
1761 * 'lgtm' - LGTM from at least one approved reviewer
1762 * 'commit' - in the commit queue
1763 * 'closed' - closed
1764 """
1765 if not self.GetIssue():
1766 return None
1767
1768 try:
1769 props = self.GetIssueProperties()
1770 except urllib2.HTTPError:
1771 return 'error'
1772
1773 if props.get('closed'):
1774 # Issue is closed.
1775 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001776 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001777 # Issue is in the commit queue.
1778 return 'commit'
1779
1780 try:
1781 reviewers = self.GetApprovingReviewers()
1782 except urllib2.HTTPError:
1783 return 'error'
1784
1785 if reviewers:
1786 # Was LGTM'ed.
1787 return 'lgtm'
1788
1789 messages = props.get('messages') or []
1790
tandrii9d2c7a32016-06-22 03:42:45 -07001791 # Skip CQ messages that don't require owner's action.
1792 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1793 if 'Dry run:' in messages[-1]['text']:
1794 messages.pop()
1795 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1796 # This message always follows prior messages from CQ,
1797 # so skip this too.
1798 messages.pop()
1799 else:
1800 # This is probably a CQ messages warranting user attention.
1801 break
1802
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001803 if not messages:
1804 # No message was sent.
1805 return 'unsent'
1806 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07001807 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001808 return 'reply'
1809 return 'waiting'
1810
dsansomee2d6fd92016-09-08 00:10:47 -07001811 def UpdateDescriptionRemote(self, description, force=False):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001812 return self.RpcServer().update_description(
1813 self.GetIssue(), self.description)
1814
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001815 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001816 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001817
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001818 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07001819 return self.SetFlags({flag: value})
1820
1821 def SetFlags(self, flags):
1822 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07001823 """
phajdan.jr68598232016-08-10 03:28:28 -07001824 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001825 try:
tandrii4b233bd2016-07-06 03:50:29 -07001826 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07001827 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07001828 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001829 if e.code == 404:
1830 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1831 if e.code == 403:
1832 DieWithError(
1833 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07001834 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001835 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001836
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001837 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001838 """Returns an upload.RpcServer() to access this review's rietveld instance.
1839 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001840 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001841 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001842 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001843 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001844 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001845
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001846 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001847 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001848 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001849
tandrii5d48c322016-08-18 16:19:37 -07001850 @classmethod
1851 def PatchsetConfigKey(cls):
1852 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001853
tandrii5d48c322016-08-18 16:19:37 -07001854 @classmethod
1855 def CodereviewServerConfigKey(cls):
1856 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001857
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001858 def GetRieveldObjForPresubmit(self):
1859 return self.RpcServer()
1860
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001861 def SetCQState(self, new_state):
1862 props = self.GetIssueProperties()
1863 if props.get('private'):
1864 DieWithError('Cannot set-commit on private issue')
1865
1866 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07001867 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001868 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07001869 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001870 else:
tandrii4b233bd2016-07-06 03:50:29 -07001871 assert new_state == _CQState.DRY_RUN
1872 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001873
1874
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001875 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1876 directory):
1877 # TODO(maruel): Use apply_issue.py
1878
1879 # PatchIssue should never be called with a dirty tree. It is up to the
1880 # caller to check this, but just in case we assert here since the
1881 # consequences of the caller not checking this could be dire.
1882 assert(not git_common.is_dirty_git_tree('apply'))
1883 assert(parsed_issue_arg.valid)
1884 self._changelist.issue = parsed_issue_arg.issue
1885 if parsed_issue_arg.hostname:
1886 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1887
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001888 if (isinstance(parsed_issue_arg, _RietveldParsedIssueNumberArgument) and
1889 parsed_issue_arg.patch_url):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001890 assert parsed_issue_arg.patchset
1891 patchset = parsed_issue_arg.patchset
1892 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1893 else:
1894 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1895 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1896
1897 # Switch up to the top-level directory, if necessary, in preparation for
1898 # applying the patch.
1899 top = settings.GetRelativeRoot()
1900 if top:
1901 os.chdir(top)
1902
1903 # Git patches have a/ at the beginning of source paths. We strip that out
1904 # with a sed script rather than the -p flag to patch so we can feed either
1905 # Git or svn-style patches into the same apply command.
1906 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1907 try:
1908 patch_data = subprocess2.check_output(
1909 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1910 except subprocess2.CalledProcessError:
1911 DieWithError('Git patch mungling failed.')
1912 logging.info(patch_data)
1913
1914 # We use "git apply" to apply the patch instead of "patch" so that we can
1915 # pick up file adds.
1916 # The --index flag means: also insert into the index (so we catch adds).
1917 cmd = ['git', 'apply', '--index', '-p0']
1918 if directory:
1919 cmd.extend(('--directory', directory))
1920 if reject:
1921 cmd.append('--reject')
1922 elif IsGitVersionAtLeast('1.7.12'):
1923 cmd.append('--3way')
1924 try:
1925 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
1926 stdin=patch_data, stdout=subprocess2.VOID)
1927 except subprocess2.CalledProcessError:
vapiera7fbd5a2016-06-16 09:17:49 -07001928 print('Failed to apply the patch')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001929 return 1
1930
1931 # If we had an issue, commit the current state and register the issue.
1932 if not nocommit:
1933 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
1934 'patch from issue %(i)s at patchset '
1935 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
1936 % {'i': self.GetIssue(), 'p': patchset})])
1937 self.SetIssue(self.GetIssue())
1938 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07001939 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001940 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001941 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001942 return 0
1943
1944 @staticmethod
1945 def ParseIssueURL(parsed_url):
1946 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1947 return None
wychen3c1c1722016-08-04 11:46:36 -07001948 # Rietveld patch: https://domain/<number>/#ps<patchset>
1949 match = re.match(r'/(\d+)/$', parsed_url.path)
1950 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
1951 if match and match2:
1952 return _RietveldParsedIssueNumberArgument(
1953 issue=int(match.group(1)),
1954 patchset=int(match2.group(1)),
1955 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001956 # Typical url: https://domain/<issue_number>[/[other]]
1957 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
1958 if match:
1959 return _RietveldParsedIssueNumberArgument(
1960 issue=int(match.group(1)),
1961 hostname=parsed_url.netloc)
1962 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
1963 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
1964 if match:
1965 return _RietveldParsedIssueNumberArgument(
1966 issue=int(match.group(1)),
1967 patchset=int(match.group(2)),
1968 hostname=parsed_url.netloc,
1969 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
1970 return None
1971
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001972 def CMDUploadChange(self, options, args, change):
1973 """Upload the patch to Rietveld."""
1974 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1975 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001976 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
1977 if options.emulate_svn_auto_props:
1978 upload_args.append('--emulate_svn_auto_props')
1979
1980 change_desc = None
1981
1982 if options.email is not None:
1983 upload_args.extend(['--email', options.email])
1984
1985 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07001986 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001987 upload_args.extend(['--title', options.title])
1988 if options.message:
1989 upload_args.extend(['--message', options.message])
1990 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07001991 print('This branch is associated with issue %s. '
1992 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001993 else:
nodirca166002016-06-27 10:59:51 -07001994 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001995 upload_args.extend(['--title', options.title])
1996 message = (options.title or options.message or
1997 CreateDescriptionFromLog(args))
1998 change_desc = ChangeDescription(message)
1999 if options.reviewers or options.tbr_owners:
2000 change_desc.update_reviewers(options.reviewers,
2001 options.tbr_owners,
2002 change)
2003 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002004 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002005
2006 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002007 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002008 return 1
2009
2010 upload_args.extend(['--message', change_desc.description])
2011 if change_desc.get_reviewers():
2012 upload_args.append('--reviewers=%s' % ','.join(
2013 change_desc.get_reviewers()))
2014 if options.send_mail:
2015 if not change_desc.get_reviewers():
2016 DieWithError("Must specify reviewers to send email.")
2017 upload_args.append('--send_mail')
2018
2019 # We check this before applying rietveld.private assuming that in
2020 # rietveld.cc only addresses which we can send private CLs to are listed
2021 # if rietveld.private is set, and so we should ignore rietveld.cc only
2022 # when --private is specified explicitly on the command line.
2023 if options.private:
2024 logging.warn('rietveld.cc is ignored since private flag is specified. '
2025 'You need to review and add them manually if necessary.')
2026 cc = self.GetCCListWithoutDefault()
2027 else:
2028 cc = self.GetCCList()
2029 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
2030 if cc:
2031 upload_args.extend(['--cc', cc])
2032
2033 if options.private or settings.GetDefaultPrivateFlag() == "True":
2034 upload_args.append('--private')
2035
2036 upload_args.extend(['--git_similarity', str(options.similarity)])
2037 if not options.find_copies:
2038 upload_args.extend(['--git_no_find_copies'])
2039
2040 # Include the upstream repo's URL in the change -- this is useful for
2041 # projects that have their source spread across multiple repos.
2042 remote_url = self.GetGitBaseUrlFromConfig()
2043 if not remote_url:
2044 if settings.GetIsGitSvn():
2045 remote_url = self.GetGitSvnRemoteUrl()
2046 else:
2047 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2048 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2049 self.GetUpstreamBranch().split('/')[-1])
2050 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002051 remote, remote_branch = self.GetRemoteBranch()
2052 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2053 settings.GetPendingRefPrefix())
2054 if target_ref:
2055 upload_args.extend(['--target_ref', target_ref])
2056
2057 # Look for dependent patchsets. See crbug.com/480453 for more details.
2058 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2059 upstream_branch = ShortBranchName(upstream_branch)
2060 if remote is '.':
2061 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002062 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002063 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002064 print()
2065 print('Skipping dependency patchset upload because git config '
2066 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2067 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002068 else:
2069 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002070 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002071 auth_config=auth_config)
2072 branch_cl_issue_url = branch_cl.GetIssueURL()
2073 branch_cl_issue = branch_cl.GetIssue()
2074 branch_cl_patchset = branch_cl.GetPatchset()
2075 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2076 upload_args.extend(
2077 ['--depends_on_patchset', '%s:%s' % (
2078 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002079 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002080 '\n'
2081 'The current branch (%s) is tracking a local branch (%s) with '
2082 'an associated CL.\n'
2083 'Adding %s/#ps%s as a dependency patchset.\n'
2084 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2085 branch_cl_patchset))
2086
2087 project = settings.GetProject()
2088 if project:
2089 upload_args.extend(['--project', project])
2090
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002091 try:
2092 upload_args = ['upload'] + upload_args + args
2093 logging.info('upload.RealMain(%s)', upload_args)
2094 issue, patchset = upload.RealMain(upload_args)
2095 issue = int(issue)
2096 patchset = int(patchset)
2097 except KeyboardInterrupt:
2098 sys.exit(1)
2099 except:
2100 # If we got an exception after the user typed a description for their
2101 # change, back up the description before re-raising.
2102 if change_desc:
2103 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2104 print('\nGot exception while uploading -- saving description to %s\n' %
2105 backup_path)
2106 backup_file = open(backup_path, 'w')
2107 backup_file.write(change_desc.description)
2108 backup_file.close()
2109 raise
2110
2111 if not self.GetIssue():
2112 self.SetIssue(issue)
2113 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002114 return 0
2115
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002116
2117class _GerritChangelistImpl(_ChangelistCodereviewBase):
2118 def __init__(self, changelist, auth_config=None):
2119 # auth_config is Rietveld thing, kept here to preserve interface only.
2120 super(_GerritChangelistImpl, self).__init__(changelist)
2121 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002122 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002123 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002124 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002125
2126 def _GetGerritHost(self):
2127 # Lazy load of configs.
2128 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002129 if self._gerrit_host and '.' not in self._gerrit_host:
2130 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2131 # This happens for internal stuff http://crbug.com/614312.
2132 parsed = urlparse.urlparse(self.GetRemoteUrl())
2133 if parsed.scheme == 'sso':
2134 print('WARNING: using non https URLs for remote is likely broken\n'
2135 ' Your current remote is: %s' % self.GetRemoteUrl())
2136 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2137 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002138 return self._gerrit_host
2139
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002140 def _GetGitHost(self):
2141 """Returns git host to be used when uploading change to Gerrit."""
2142 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2143
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002144 def GetCodereviewServer(self):
2145 if not self._gerrit_server:
2146 # If we're on a branch then get the server potentially associated
2147 # with that branch.
2148 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002149 self._gerrit_server = self._GitGetBranchConfigValue(
2150 self.CodereviewServerConfigKey())
2151 if self._gerrit_server:
2152 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002153 if not self._gerrit_server:
2154 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2155 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002156 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002157 parts[0] = parts[0] + '-review'
2158 self._gerrit_host = '.'.join(parts)
2159 self._gerrit_server = 'https://%s' % self._gerrit_host
2160 return self._gerrit_server
2161
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002162 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002163 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002164 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002165
tandrii5d48c322016-08-18 16:19:37 -07002166 @classmethod
2167 def PatchsetConfigKey(cls):
2168 return 'gerritpatchset'
2169
2170 @classmethod
2171 def CodereviewServerConfigKey(cls):
2172 return 'gerritserver'
2173
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002174 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002175 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002176 if settings.GetGerritSkipEnsureAuthenticated():
2177 # For projects with unusual authentication schemes.
2178 # See http://crbug.com/603378.
2179 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002180 # Lazy-loader to identify Gerrit and Git hosts.
2181 if gerrit_util.GceAuthenticator.is_gce():
2182 return
2183 self.GetCodereviewServer()
2184 git_host = self._GetGitHost()
2185 assert self._gerrit_server and self._gerrit_host
2186 cookie_auth = gerrit_util.CookiesAuthenticator()
2187
2188 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2189 git_auth = cookie_auth.get_auth_header(git_host)
2190 if gerrit_auth and git_auth:
2191 if gerrit_auth == git_auth:
2192 return
2193 print((
2194 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2195 ' Check your %s or %s file for credentials of hosts:\n'
2196 ' %s\n'
2197 ' %s\n'
2198 ' %s') %
2199 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2200 git_host, self._gerrit_host,
2201 cookie_auth.get_new_password_message(git_host)))
2202 if not force:
2203 ask_for_data('If you know what you are doing, press Enter to continue, '
2204 'Ctrl+C to abort.')
2205 return
2206 else:
2207 missing = (
2208 [] if gerrit_auth else [self._gerrit_host] +
2209 [] if git_auth else [git_host])
2210 DieWithError('Credentials for the following hosts are required:\n'
2211 ' %s\n'
2212 'These are read from %s (or legacy %s)\n'
2213 '%s' % (
2214 '\n '.join(missing),
2215 cookie_auth.get_gitcookies_path(),
2216 cookie_auth.get_netrc_path(),
2217 cookie_auth.get_new_password_message(git_host)))
2218
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002219 def _PostUnsetIssueProperties(self):
2220 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002221 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002222
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002223 def GetRieveldObjForPresubmit(self):
2224 class ThisIsNotRietveldIssue(object):
2225 def __nonzero__(self):
2226 # This is a hack to make presubmit_support think that rietveld is not
2227 # defined, yet still ensure that calls directly result in a decent
2228 # exception message below.
2229 return False
2230
2231 def __getattr__(self, attr):
2232 print(
2233 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2234 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2235 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2236 'or use Rietveld for codereview.\n'
2237 'See also http://crbug.com/579160.' % attr)
2238 raise NotImplementedError()
2239 return ThisIsNotRietveldIssue()
2240
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002241 def GetGerritObjForPresubmit(self):
2242 return presubmit_support.GerritAccessor(self._GetGerritHost())
2243
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002244 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002245 """Apply a rough heuristic to give a simple summary of an issue's review
2246 or CQ status, assuming adherence to a common workflow.
2247
2248 Returns None if no issue for this branch, or one of the following keywords:
2249 * 'error' - error from review tool (including deleted issues)
2250 * 'unsent' - no reviewers added
2251 * 'waiting' - waiting for review
2252 * 'reply' - waiting for owner to reply to review
2253 * 'not lgtm' - Code-Review -2 from at least one approved reviewer
2254 * 'lgtm' - Code-Review +2 from at least one approved reviewer
2255 * 'commit' - in the commit queue
2256 * 'closed' - abandoned
2257 """
2258 if not self.GetIssue():
2259 return None
2260
2261 try:
2262 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
2263 except httplib.HTTPException:
2264 return 'error'
2265
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002266 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002267 return 'closed'
2268
2269 cq_label = data['labels'].get('Commit-Queue', {})
2270 if cq_label:
2271 # Vote value is a stringified integer, which we expect from 0 to 2.
2272 vote_value = cq_label.get('value', '0')
2273 vote_text = cq_label.get('values', {}).get(vote_value, '')
2274 if vote_text.lower() == 'commit':
2275 return 'commit'
2276
2277 lgtm_label = data['labels'].get('Code-Review', {})
2278 if lgtm_label:
2279 if 'rejected' in lgtm_label:
2280 return 'not lgtm'
2281 if 'approved' in lgtm_label:
2282 return 'lgtm'
2283
2284 if not data.get('reviewers', {}).get('REVIEWER', []):
2285 return 'unsent'
2286
2287 messages = data.get('messages', [])
2288 if messages:
2289 owner = data['owner'].get('_account_id')
2290 last_message_author = messages[-1].get('author', {}).get('_account_id')
2291 if owner != last_message_author:
2292 # Some reply from non-owner.
2293 return 'reply'
2294
2295 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002296
2297 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002298 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002299 return data['revisions'][data['current_revision']]['_number']
2300
2301 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002302 data = self._GetChangeDetail(['CURRENT_REVISION'])
2303 current_rev = data['current_revision']
2304 url = data['revisions'][current_rev]['fetch']['http']['url']
2305 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002306
dsansomee2d6fd92016-09-08 00:10:47 -07002307 def UpdateDescriptionRemote(self, description, force=False):
2308 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2309 if not force:
2310 ask_for_data(
2311 'The description cannot be modified while the issue has a pending '
2312 'unpublished edit. Either publish the edit in the Gerrit web UI '
2313 'or delete it.\n\n'
2314 'Press Enter to delete the unpublished edit, Ctrl+C to abort.')
2315
2316 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2317 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002318 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
2319 description)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002320
2321 def CloseIssue(self):
2322 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2323
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002324 def GetApprovingReviewers(self):
2325 """Returns a list of reviewers approving the change.
2326
2327 Note: not necessarily committers.
2328 """
2329 raise NotImplementedError()
2330
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002331 def SubmitIssue(self, wait_for_merge=True):
2332 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2333 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002334
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002335 def _GetChangeDetail(self, options=None, issue=None):
2336 options = options or []
2337 issue = issue or self.GetIssue()
2338 assert issue, 'issue required to query Gerrit'
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002339 return gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2340 options)
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002341
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002342 def CMDLand(self, force, bypass_hooks, verbose):
2343 if git_common.is_dirty_git_tree('land'):
2344 return 1
tandriid60367b2016-06-22 05:25:12 -07002345 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2346 if u'Commit-Queue' in detail.get('labels', {}):
2347 if not force:
2348 ask_for_data('\nIt seems this repository has a Commit Queue, '
2349 'which can test and land changes for you. '
2350 'Are you sure you wish to bypass it?\n'
2351 'Press Enter to continue, Ctrl+C to abort.')
2352
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002353 differs = True
tandriic4344b52016-08-29 06:04:54 -07002354 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002355 # Note: git diff outputs nothing if there is no diff.
2356 if not last_upload or RunGit(['diff', last_upload]).strip():
2357 print('WARNING: some changes from local branch haven\'t been uploaded')
2358 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002359 if detail['current_revision'] == last_upload:
2360 differs = False
2361 else:
2362 print('WARNING: local branch contents differ from latest uploaded '
2363 'patchset')
2364 if differs:
2365 if not force:
2366 ask_for_data(
2367 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2368 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2369 elif not bypass_hooks:
2370 hook_results = self.RunHook(
2371 committing=True,
2372 may_prompt=not force,
2373 verbose=verbose,
2374 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2375 if not hook_results.should_continue():
2376 return 1
2377
2378 self.SubmitIssue(wait_for_merge=True)
2379 print('Issue %s has been submitted.' % self.GetIssueURL())
2380 return 0
2381
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002382 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2383 directory):
2384 assert not reject
2385 assert not nocommit
2386 assert not directory
2387 assert parsed_issue_arg.valid
2388
2389 self._changelist.issue = parsed_issue_arg.issue
2390
2391 if parsed_issue_arg.hostname:
2392 self._gerrit_host = parsed_issue_arg.hostname
2393 self._gerrit_server = 'https://%s' % self._gerrit_host
2394
2395 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2396
2397 if not parsed_issue_arg.patchset:
2398 # Use current revision by default.
2399 revision_info = detail['revisions'][detail['current_revision']]
2400 patchset = int(revision_info['_number'])
2401 else:
2402 patchset = parsed_issue_arg.patchset
2403 for revision_info in detail['revisions'].itervalues():
2404 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2405 break
2406 else:
2407 DieWithError('Couldn\'t find patchset %i in issue %i' %
2408 (parsed_issue_arg.patchset, self.GetIssue()))
2409
2410 fetch_info = revision_info['fetch']['http']
2411 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2412 RunGit(['cherry-pick', 'FETCH_HEAD'])
2413 self.SetIssue(self.GetIssue())
2414 self.SetPatchset(patchset)
2415 print('Committed patch for issue %i pathset %i locally' %
2416 (self.GetIssue(), self.GetPatchset()))
2417 return 0
2418
2419 @staticmethod
2420 def ParseIssueURL(parsed_url):
2421 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2422 return None
2423 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2424 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2425 # Short urls like https://domain/<issue_number> can be used, but don't allow
2426 # specifying the patchset (you'd 404), but we allow that here.
2427 if parsed_url.path == '/':
2428 part = parsed_url.fragment
2429 else:
2430 part = parsed_url.path
2431 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2432 if match:
2433 return _ParsedIssueNumberArgument(
2434 issue=int(match.group(2)),
2435 patchset=int(match.group(4)) if match.group(4) else None,
2436 hostname=parsed_url.netloc)
2437 return None
2438
tandrii16e0b4e2016-06-07 10:34:28 -07002439 def _GerritCommitMsgHookCheck(self, offer_removal):
2440 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2441 if not os.path.exists(hook):
2442 return
2443 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2444 # custom developer made one.
2445 data = gclient_utils.FileRead(hook)
2446 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2447 return
2448 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002449 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002450 'and may interfere with it in subtle ways.\n'
2451 'We recommend you remove the commit-msg hook.')
2452 if offer_removal:
2453 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2454 if reply.lower().startswith('y'):
2455 gclient_utils.rm_file_or_tree(hook)
2456 print('Gerrit commit-msg hook removed.')
2457 else:
2458 print('OK, will keep Gerrit commit-msg hook in place.')
2459
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002460 def CMDUploadChange(self, options, args, change):
2461 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002462 if options.squash and options.no_squash:
2463 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002464
2465 if not options.squash and not options.no_squash:
2466 # Load default for user, repo, squash=true, in this order.
2467 options.squash = settings.GetSquashGerritUploads()
2468 elif options.no_squash:
2469 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002470
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002471 # We assume the remote called "origin" is the one we want.
2472 # It is probably not worthwhile to support different workflows.
2473 gerrit_remote = 'origin'
2474
2475 remote, remote_branch = self.GetRemoteBranch()
2476 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2477 pending_prefix='')
2478
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002479 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002480 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002481 if self.GetIssue():
2482 # Try to get the message from a previous upload.
2483 message = self.GetDescription()
2484 if not message:
2485 DieWithError(
2486 'failed to fetch description from current Gerrit issue %d\n'
2487 '%s' % (self.GetIssue(), self.GetIssueURL()))
2488 change_id = self._GetChangeDetail()['change_id']
2489 while True:
2490 footer_change_ids = git_footers.get_footer_change_id(message)
2491 if footer_change_ids == [change_id]:
2492 break
2493 if not footer_change_ids:
2494 message = git_footers.add_footer_change_id(message, change_id)
2495 print('WARNING: appended missing Change-Id to issue description')
2496 continue
2497 # There is already a valid footer but with different or several ids.
2498 # Doing this automatically is non-trivial as we don't want to lose
2499 # existing other footers, yet we want to append just 1 desired
2500 # Change-Id. Thus, just create a new footer, but let user verify the
2501 # new description.
2502 message = '%s\n\nChange-Id: %s' % (message, change_id)
2503 print(
2504 'WARNING: issue %s has Change-Id footer(s):\n'
2505 ' %s\n'
2506 'but issue has Change-Id %s, according to Gerrit.\n'
2507 'Please, check the proposed correction to the description, '
2508 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2509 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2510 change_id))
2511 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2512 if not options.force:
2513 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002514 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002515 message = change_desc.description
2516 if not message:
2517 DieWithError("Description is empty. Aborting...")
2518 # Continue the while loop.
2519 # Sanity check of this code - we should end up with proper message
2520 # footer.
2521 assert [change_id] == git_footers.get_footer_change_id(message)
2522 change_desc = ChangeDescription(message)
2523 else:
2524 change_desc = ChangeDescription(
2525 options.message or CreateDescriptionFromLog(args))
2526 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002527 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002528 if not change_desc.description:
2529 DieWithError("Description is empty. Aborting...")
2530 message = change_desc.description
2531 change_ids = git_footers.get_footer_change_id(message)
2532 if len(change_ids) > 1:
2533 DieWithError('too many Change-Id footers, at most 1 allowed.')
2534 if not change_ids:
2535 # Generate the Change-Id automatically.
2536 message = git_footers.add_footer_change_id(
2537 message, GenerateGerritChangeId(message))
2538 change_desc.set_description(message)
2539 change_ids = git_footers.get_footer_change_id(message)
2540 assert len(change_ids) == 1
2541 change_id = change_ids[0]
2542
2543 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2544 if remote is '.':
2545 # If our upstream branch is local, we base our squashed commit on its
2546 # squashed version.
2547 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2548 # Check the squashed hash of the parent.
2549 parent = RunGit(['config',
2550 'branch.%s.gerritsquashhash' % upstream_branch_name],
2551 error_ok=True).strip()
2552 # Verify that the upstream branch has been uploaded too, otherwise
2553 # Gerrit will create additional CLs when uploading.
2554 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2555 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002556 DieWithError(
2557 'Upload upstream branch %s first.\n'
tandrii2bdadf12016-07-12 12:27:54 -07002558 'Note: maybe you\'ve uploaded it with --no-squash. '
2559 'If so, then re-upload it with:\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002560 ' git cl upload --squash\n' % upstream_branch_name)
2561 else:
2562 parent = self.GetCommonAncestorWithUpstream()
2563
2564 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2565 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2566 '-m', message]).strip()
2567 else:
2568 change_desc = ChangeDescription(
2569 options.message or CreateDescriptionFromLog(args))
2570 if not change_desc.description:
2571 DieWithError("Description is empty. Aborting...")
2572
2573 if not git_footers.get_footer_change_id(change_desc.description):
2574 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002575 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2576 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002577 ref_to_push = 'HEAD'
2578 parent = '%s/%s' % (gerrit_remote, branch)
2579 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2580
2581 assert change_desc
2582 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2583 ref_to_push)]).splitlines()
2584 if len(commits) > 1:
2585 print('WARNING: This will upload %d commits. Run the following command '
2586 'to see which commits will be uploaded: ' % len(commits))
2587 print('git log %s..%s' % (parent, ref_to_push))
2588 print('You can also use `git squash-branch` to squash these into a '
2589 'single commit.')
2590 ask_for_data('About to upload; enter to confirm.')
2591
2592 if options.reviewers or options.tbr_owners:
2593 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2594 change)
2595
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002596 # Extra options that can be specified at push time. Doc:
2597 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2598 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002599 if change_desc.get_reviewers(tbr_only=True):
2600 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2601 refspec_opts.append('l=Code-Review+1')
2602
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002603 if options.title:
tandriieefe8322016-08-17 10:12:24 -07002604 if not re.match(r'^[\w ]+$', options.title):
2605 options.title = re.sub(r'[^\w ]', '', options.title)
2606 print('WARNING: Patchset title may only contain alphanumeric chars '
2607 'and spaces. Cleaned up title:\n%s' % options.title)
2608 if not options.force:
2609 ask_for_data('Press enter to continue, Ctrl+C to abort')
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002610 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2611 # reverse on its side.
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002612 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2613
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002614 if options.send_mail:
2615 if not change_desc.get_reviewers():
2616 DieWithError('Must specify reviewers to send email.')
2617 refspec_opts.append('notify=ALL')
2618 else:
2619 refspec_opts.append('notify=NONE')
2620
tandrii99a72f22016-08-17 14:33:24 -07002621 reviewers = change_desc.get_reviewers()
2622 if reviewers:
2623 refspec_opts.extend('r=' + email.strip() for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002624
agablec6787972016-09-09 16:13:34 -07002625 if options.private:
2626 refspec_opts.append('draft')
2627
rmistry9eadede2016-09-19 11:22:43 -07002628 if options.topic:
2629 # Documentation on Gerrit topics is here:
2630 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
2631 refspec_opts.append('topic=%s' % options.topic)
2632
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002633 refspec_suffix = ''
2634 if refspec_opts:
2635 refspec_suffix = '%' + ','.join(refspec_opts)
2636 assert ' ' not in refspec_suffix, (
2637 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002638 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002639
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002640 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002641 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002642 print_stdout=True,
2643 # Flush after every line: useful for seeing progress when running as
2644 # recipe.
2645 filter_fn=lambda _: sys.stdout.flush())
2646
2647 if options.squash:
2648 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2649 change_numbers = [m.group(1)
2650 for m in map(regex.match, push_stdout.splitlines())
2651 if m]
2652 if len(change_numbers) != 1:
2653 DieWithError(
2654 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2655 'Change-Id: %s') % (len(change_numbers), change_id))
2656 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002657 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002658
2659 # Add cc's from the CC_LIST and --cc flag (if any).
2660 cc = self.GetCCList().split(',')
2661 if options.cc:
2662 cc.extend(options.cc)
2663 cc = filter(None, [email.strip() for email in cc])
2664 if cc:
2665 gerrit_util.AddReviewers(
2666 self._GetGerritHost(), self.GetIssue(), cc, is_reviewer=False)
2667
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002668 return 0
2669
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002670 def _AddChangeIdToCommitMessage(self, options, args):
2671 """Re-commits using the current message, assumes the commit hook is in
2672 place.
2673 """
2674 log_desc = options.message or CreateDescriptionFromLog(args)
2675 git_command = ['commit', '--amend', '-m', log_desc]
2676 RunGit(git_command)
2677 new_log_desc = CreateDescriptionFromLog(args)
2678 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002679 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002680 return new_log_desc
2681 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002682 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002683
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002684 def SetCQState(self, new_state):
2685 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002686 vote_map = {
2687 _CQState.NONE: 0,
2688 _CQState.DRY_RUN: 1,
2689 _CQState.COMMIT : 2,
2690 }
2691 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2692 labels={'Commit-Queue': vote_map[new_state]})
2693
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002694
2695_CODEREVIEW_IMPLEMENTATIONS = {
2696 'rietveld': _RietveldChangelistImpl,
2697 'gerrit': _GerritChangelistImpl,
2698}
2699
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002700
iannuccie53c9352016-08-17 14:40:40 -07002701def _add_codereview_issue_select_options(parser, extra=""):
2702 _add_codereview_select_options(parser)
2703
2704 text = ('Operate on this issue number instead of the current branch\'s '
2705 'implicit issue.')
2706 if extra:
2707 text += ' '+extra
2708 parser.add_option('-i', '--issue', type=int, help=text)
2709
2710
2711def _process_codereview_issue_select_options(parser, options):
2712 _process_codereview_select_options(parser, options)
2713 if options.issue is not None and not options.forced_codereview:
2714 parser.error('--issue must be specified with either --rietveld or --gerrit')
2715
2716
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002717def _add_codereview_select_options(parser):
2718 """Appends --gerrit and --rietveld options to force specific codereview."""
2719 parser.codereview_group = optparse.OptionGroup(
2720 parser, 'EXPERIMENTAL! Codereview override options')
2721 parser.add_option_group(parser.codereview_group)
2722 parser.codereview_group.add_option(
2723 '--gerrit', action='store_true',
2724 help='Force the use of Gerrit for codereview')
2725 parser.codereview_group.add_option(
2726 '--rietveld', action='store_true',
2727 help='Force the use of Rietveld for codereview')
2728
2729
2730def _process_codereview_select_options(parser, options):
2731 if options.gerrit and options.rietveld:
2732 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2733 options.forced_codereview = None
2734 if options.gerrit:
2735 options.forced_codereview = 'gerrit'
2736 elif options.rietveld:
2737 options.forced_codereview = 'rietveld'
2738
2739
tandriif9aefb72016-07-01 09:06:51 -07002740def _get_bug_line_values(default_project, bugs):
2741 """Given default_project and comma separated list of bugs, yields bug line
2742 values.
2743
2744 Each bug can be either:
2745 * a number, which is combined with default_project
2746 * string, which is left as is.
2747
2748 This function may produce more than one line, because bugdroid expects one
2749 project per line.
2750
2751 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2752 ['v8:123', 'chromium:789']
2753 """
2754 default_bugs = []
2755 others = []
2756 for bug in bugs.split(','):
2757 bug = bug.strip()
2758 if bug:
2759 try:
2760 default_bugs.append(int(bug))
2761 except ValueError:
2762 others.append(bug)
2763
2764 if default_bugs:
2765 default_bugs = ','.join(map(str, default_bugs))
2766 if default_project:
2767 yield '%s:%s' % (default_project, default_bugs)
2768 else:
2769 yield default_bugs
2770 for other in sorted(others):
2771 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2772 yield other
2773
2774
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002775class ChangeDescription(object):
2776 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002777 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002778 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002779
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002780 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002781 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002782
agable@chromium.org42c20792013-09-12 17:34:49 +00002783 @property # www.logilab.org/ticket/89786
2784 def description(self): # pylint: disable=E0202
2785 return '\n'.join(self._description_lines)
2786
2787 def set_description(self, desc):
2788 if isinstance(desc, basestring):
2789 lines = desc.splitlines()
2790 else:
2791 lines = [line.rstrip() for line in desc]
2792 while lines and not lines[0]:
2793 lines.pop(0)
2794 while lines and not lines[-1]:
2795 lines.pop(-1)
2796 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002797
piman@chromium.org336f9122014-09-04 02:16:55 +00002798 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002799 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002800 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002801 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002802 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002803 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002804
agable@chromium.org42c20792013-09-12 17:34:49 +00002805 # Get the set of R= and TBR= lines and remove them from the desciption.
2806 regexp = re.compile(self.R_LINE)
2807 matches = [regexp.match(line) for line in self._description_lines]
2808 new_desc = [l for i, l in enumerate(self._description_lines)
2809 if not matches[i]]
2810 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002811
agable@chromium.org42c20792013-09-12 17:34:49 +00002812 # Construct new unified R= and TBR= lines.
2813 r_names = []
2814 tbr_names = []
2815 for match in matches:
2816 if not match:
2817 continue
2818 people = cleanup_list([match.group(2).strip()])
2819 if match.group(1) == 'TBR':
2820 tbr_names.extend(people)
2821 else:
2822 r_names.extend(people)
2823 for name in r_names:
2824 if name not in reviewers:
2825 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002826 if add_owners_tbr:
2827 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07002828 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00002829 all_reviewers = set(tbr_names + reviewers)
2830 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2831 all_reviewers)
2832 tbr_names.extend(owners_db.reviewers_for(missing_files,
2833 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002834 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2835 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2836
2837 # Put the new lines in the description where the old first R= line was.
2838 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2839 if 0 <= line_loc < len(self._description_lines):
2840 if new_tbr_line:
2841 self._description_lines.insert(line_loc, new_tbr_line)
2842 if new_r_line:
2843 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002844 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002845 if new_r_line:
2846 self.append_footer(new_r_line)
2847 if new_tbr_line:
2848 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002849
tandriif9aefb72016-07-01 09:06:51 -07002850 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002851 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002852 self.set_description([
2853 '# Enter a description of the change.',
2854 '# This will be displayed on the codereview site.',
2855 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002856 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002857 '--------------------',
2858 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002859
agable@chromium.org42c20792013-09-12 17:34:49 +00002860 regexp = re.compile(self.BUG_LINE)
2861 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07002862 prefix = settings.GetBugPrefix()
2863 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
2864 for value in values:
2865 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
2866 self.append_footer('BUG=%s' % value)
2867
agable@chromium.org42c20792013-09-12 17:34:49 +00002868 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002869 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002870 if not content:
2871 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002872 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002873
2874 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002875 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2876 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002877 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002878 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002879
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002880 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00002881 """Adds a footer line to the description.
2882
2883 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
2884 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
2885 that Gerrit footers are always at the end.
2886 """
2887 parsed_footer_line = git_footers.parse_footer(line)
2888 if parsed_footer_line:
2889 # Line is a gerrit footer in the form: Footer-Key: any value.
2890 # Thus, must be appended observing Gerrit footer rules.
2891 self.set_description(
2892 git_footers.add_footer(self.description,
2893 key=parsed_footer_line[0],
2894 value=parsed_footer_line[1]))
2895 return
2896
2897 if not self._description_lines:
2898 self._description_lines.append(line)
2899 return
2900
2901 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
2902 if gerrit_footers:
2903 # git_footers.split_footers ensures that there is an empty line before
2904 # actual (gerrit) footers, if any. We have to keep it that way.
2905 assert top_lines and top_lines[-1] == ''
2906 top_lines, separator = top_lines[:-1], top_lines[-1:]
2907 else:
2908 separator = [] # No need for separator if there are no gerrit_footers.
2909
2910 prev_line = top_lines[-1] if top_lines else ''
2911 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
2912 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2913 top_lines.append('')
2914 top_lines.append(line)
2915 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002916
tandrii99a72f22016-08-17 14:33:24 -07002917 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002918 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002919 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07002920 reviewers = [match.group(2).strip()
2921 for match in matches
2922 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002923 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002924
2925
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002926def get_approving_reviewers(props):
2927 """Retrieves the reviewers that approved a CL from the issue properties with
2928 messages.
2929
2930 Note that the list may contain reviewers that are not committer, thus are not
2931 considered by the CQ.
2932 """
2933 return sorted(
2934 set(
2935 message['sender']
2936 for message in props['messages']
2937 if message['approval'] and message['sender'] in props['reviewers']
2938 )
2939 )
2940
2941
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002942def FindCodereviewSettingsFile(filename='codereview.settings'):
2943 """Finds the given file starting in the cwd and going up.
2944
2945 Only looks up to the top of the repository unless an
2946 'inherit-review-settings-ok' file exists in the root of the repository.
2947 """
2948 inherit_ok_file = 'inherit-review-settings-ok'
2949 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002950 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002951 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2952 root = '/'
2953 while True:
2954 if filename in os.listdir(cwd):
2955 if os.path.isfile(os.path.join(cwd, filename)):
2956 return open(os.path.join(cwd, filename))
2957 if cwd == root:
2958 break
2959 cwd = os.path.dirname(cwd)
2960
2961
2962def LoadCodereviewSettingsFromFile(fileobj):
2963 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002964 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002965
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002966 def SetProperty(name, setting, unset_error_ok=False):
2967 fullname = 'rietveld.' + name
2968 if setting in keyvals:
2969 RunGit(['config', fullname, keyvals[setting]])
2970 else:
2971 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2972
2973 SetProperty('server', 'CODE_REVIEW_SERVER')
2974 # Only server setting is required. Other settings can be absent.
2975 # In that case, we ignore errors raised during option deletion attempt.
2976 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002977 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002978 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2979 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002980 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002981 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002982 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2983 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002984 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002985 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002986 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
tandriif46c20f2016-09-14 06:17:05 -07002987 SetProperty('git-number-footer', 'GIT_NUMBER_FOOTER', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002988 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2989 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002990
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002991 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002992 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002993
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002994 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07002995 RunGit(['config', 'gerrit.squash-uploads',
2996 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002997
tandrii@chromium.org28253532016-04-14 13:46:56 +00002998 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00002999 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003000 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3001
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003002 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
3003 #should be of the form
3004 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3005 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
3006 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3007 keyvals['ORIGIN_URL_CONFIG']])
3008
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003009
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003010def urlretrieve(source, destination):
3011 """urllib is broken for SSL connections via a proxy therefore we
3012 can't use urllib.urlretrieve()."""
3013 with open(destination, 'w') as f:
3014 f.write(urllib2.urlopen(source).read())
3015
3016
ukai@chromium.org712d6102013-11-27 00:52:58 +00003017def hasSheBang(fname):
3018 """Checks fname is a #! script."""
3019 with open(fname) as f:
3020 return f.read(2).startswith('#!')
3021
3022
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003023# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3024def DownloadHooks(*args, **kwargs):
3025 pass
3026
3027
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003028def DownloadGerritHook(force):
3029 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003030
3031 Args:
3032 force: True to update hooks. False to install hooks if not present.
3033 """
3034 if not settings.GetIsGerrit():
3035 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003036 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003037 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3038 if not os.access(dst, os.X_OK):
3039 if os.path.exists(dst):
3040 if not force:
3041 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003042 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003043 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003044 if not hasSheBang(dst):
3045 DieWithError('Not a script: %s\n'
3046 'You need to download from\n%s\n'
3047 'into .git/hooks/commit-msg and '
3048 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003049 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3050 except Exception:
3051 if os.path.exists(dst):
3052 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003053 DieWithError('\nFailed to download hooks.\n'
3054 'You need to download from\n%s\n'
3055 'into .git/hooks/commit-msg and '
3056 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003057
3058
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003059
3060def GetRietveldCodereviewSettingsInteractively():
3061 """Prompt the user for settings."""
3062 server = settings.GetDefaultServerUrl(error_ok=True)
3063 prompt = 'Rietveld server (host[:port])'
3064 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3065 newserver = ask_for_data(prompt + ':')
3066 if not server and not newserver:
3067 newserver = DEFAULT_SERVER
3068 if newserver:
3069 newserver = gclient_utils.UpgradeToHttps(newserver)
3070 if newserver != server:
3071 RunGit(['config', 'rietveld.server', newserver])
3072
3073 def SetProperty(initial, caption, name, is_url):
3074 prompt = caption
3075 if initial:
3076 prompt += ' ("x" to clear) [%s]' % initial
3077 new_val = ask_for_data(prompt + ':')
3078 if new_val == 'x':
3079 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3080 elif new_val:
3081 if is_url:
3082 new_val = gclient_utils.UpgradeToHttps(new_val)
3083 if new_val != initial:
3084 RunGit(['config', 'rietveld.' + name, new_val])
3085
3086 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3087 SetProperty(settings.GetDefaultPrivateFlag(),
3088 'Private flag (rietveld only)', 'private', False)
3089 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3090 'tree-status-url', False)
3091 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3092 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3093 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3094 'run-post-upload-hook', False)
3095
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003096@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003097def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003098 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003099
tandrii5d0a0422016-09-14 06:24:35 -07003100 print('WARNING: git cl config works for Rietveld only')
3101 # TODO(tandrii): remove this once we switch to Gerrit.
3102 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003103 parser.add_option('--activate-update', action='store_true',
3104 help='activate auto-updating [rietveld] section in '
3105 '.git/config')
3106 parser.add_option('--deactivate-update', action='store_true',
3107 help='deactivate auto-updating [rietveld] section in '
3108 '.git/config')
3109 options, args = parser.parse_args(args)
3110
3111 if options.deactivate_update:
3112 RunGit(['config', 'rietveld.autoupdate', 'false'])
3113 return
3114
3115 if options.activate_update:
3116 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3117 return
3118
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003119 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003120 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003121 return 0
3122
3123 url = args[0]
3124 if not url.endswith('codereview.settings'):
3125 url = os.path.join(url, 'codereview.settings')
3126
3127 # Load code review settings and download hooks (if available).
3128 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3129 return 0
3130
3131
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003132def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003133 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003134 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3135 branch = ShortBranchName(branchref)
3136 _, args = parser.parse_args(args)
3137 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003138 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003139 return RunGit(['config', 'branch.%s.base-url' % branch],
3140 error_ok=False).strip()
3141 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003142 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003143 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3144 error_ok=False).strip()
3145
3146
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003147def color_for_status(status):
3148 """Maps a Changelist status to color, for CMDstatus and other tools."""
3149 return {
3150 'unsent': Fore.RED,
3151 'waiting': Fore.BLUE,
3152 'reply': Fore.YELLOW,
3153 'lgtm': Fore.GREEN,
3154 'commit': Fore.MAGENTA,
3155 'closed': Fore.CYAN,
3156 'error': Fore.WHITE,
3157 }.get(status, Fore.WHITE)
3158
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003159
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003160def get_cl_statuses(changes, fine_grained, max_processes=None):
3161 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003162
3163 If fine_grained is true, this will fetch CL statuses from the server.
3164 Otherwise, simply indicate if there's a matching url for the given branches.
3165
3166 If max_processes is specified, it is used as the maximum number of processes
3167 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3168 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003169
3170 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003171 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003172 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003173 upload.verbosity = 0
3174
3175 if fine_grained:
3176 # Process one branch synchronously to work through authentication, then
3177 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003178 if changes:
tandriiea9514a2016-08-17 12:32:37 -07003179 def fetch(cl):
3180 try:
3181 return (cl, cl.GetStatus())
3182 except:
3183 # See http://crbug.com/629863.
3184 logging.exception('failed to fetch status for %s:', cl)
3185 raise
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003186 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003187
tandriiea9514a2016-08-17 12:32:37 -07003188 changes_to_fetch = changes[1:]
3189 if not changes_to_fetch:
kmarshall3bff56b2016-06-06 18:31:47 -07003190 # Exit early if there was only one branch to fetch.
3191 return
3192
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003193 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003194 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003195 if max_processes is not None
dsinclair99d30172016-08-09 10:48:58 -07003196 else max(len(changes_to_fetch), 1))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003197
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003198 fetched_cls = set()
3199 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003200 while True:
3201 try:
3202 row = it.next(timeout=5)
3203 except multiprocessing.TimeoutError:
3204 break
3205
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003206 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003207 yield row
3208
3209 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003210 for cl in set(changes_to_fetch) - fetched_cls:
3211 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003212
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003213 else:
3214 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003215 for cl in changes:
3216 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003217
rmistry@google.com2dd99862015-06-22 12:22:18 +00003218
3219def upload_branch_deps(cl, args):
3220 """Uploads CLs of local branches that are dependents of the current branch.
3221
3222 If the local branch dependency tree looks like:
3223 test1 -> test2.1 -> test3.1
3224 -> test3.2
3225 -> test2.2 -> test3.3
3226
3227 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3228 run on the dependent branches in this order:
3229 test2.1, test3.1, test3.2, test2.2, test3.3
3230
3231 Note: This function does not rebase your local dependent branches. Use it when
3232 you make a change to the parent branch that will not conflict with its
3233 dependent branches, and you would like their dependencies updated in
3234 Rietveld.
3235 """
3236 if git_common.is_dirty_git_tree('upload-branch-deps'):
3237 return 1
3238
3239 root_branch = cl.GetBranch()
3240 if root_branch is None:
3241 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3242 'Get on a branch!')
3243 if not cl.GetIssue() or not cl.GetPatchset():
3244 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3245 'patchset dependencies without an uploaded CL.')
3246
3247 branches = RunGit(['for-each-ref',
3248 '--format=%(refname:short) %(upstream:short)',
3249 'refs/heads'])
3250 if not branches:
3251 print('No local branches found.')
3252 return 0
3253
3254 # Create a dictionary of all local branches to the branches that are dependent
3255 # on it.
3256 tracked_to_dependents = collections.defaultdict(list)
3257 for b in branches.splitlines():
3258 tokens = b.split()
3259 if len(tokens) == 2:
3260 branch_name, tracked = tokens
3261 tracked_to_dependents[tracked].append(branch_name)
3262
vapiera7fbd5a2016-06-16 09:17:49 -07003263 print()
3264 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003265 dependents = []
3266 def traverse_dependents_preorder(branch, padding=''):
3267 dependents_to_process = tracked_to_dependents.get(branch, [])
3268 padding += ' '
3269 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003270 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003271 dependents.append(dependent)
3272 traverse_dependents_preorder(dependent, padding)
3273 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003274 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003275
3276 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003277 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003278 return 0
3279
vapiera7fbd5a2016-06-16 09:17:49 -07003280 print('This command will checkout all dependent branches and run '
3281 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003282 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3283
andybons@chromium.org962f9462016-02-03 20:00:42 +00003284 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003285 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003286 args.extend(['-t', 'Updated patchset dependency'])
3287
rmistry@google.com2dd99862015-06-22 12:22:18 +00003288 # Record all dependents that failed to upload.
3289 failures = {}
3290 # Go through all dependents, checkout the branch and upload.
3291 try:
3292 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003293 print()
3294 print('--------------------------------------')
3295 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003296 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003297 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003298 try:
3299 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003300 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003301 failures[dependent_branch] = 1
3302 except: # pylint: disable=W0702
3303 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003304 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003305 finally:
3306 # Swap back to the original root branch.
3307 RunGit(['checkout', '-q', root_branch])
3308
vapiera7fbd5a2016-06-16 09:17:49 -07003309 print()
3310 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003311 for dependent_branch in dependents:
3312 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003313 print(' %s : %s' % (dependent_branch, upload_status))
3314 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003315
3316 return 0
3317
3318
kmarshall3bff56b2016-06-06 18:31:47 -07003319def CMDarchive(parser, args):
3320 """Archives and deletes branches associated with closed changelists."""
3321 parser.add_option(
3322 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003323 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003324 parser.add_option(
3325 '-f', '--force', action='store_true',
3326 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003327 parser.add_option(
3328 '-d', '--dry-run', action='store_true',
3329 help='Skip the branch tagging and removal steps.')
3330 parser.add_option(
3331 '-t', '--notags', action='store_true',
3332 help='Do not tag archived branches. '
3333 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003334
3335 auth.add_auth_options(parser)
3336 options, args = parser.parse_args(args)
3337 if args:
3338 parser.error('Unsupported args: %s' % ' '.join(args))
3339 auth_config = auth.extract_auth_config_from_options(options)
3340
3341 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3342 if not branches:
3343 return 0
3344
vapiera7fbd5a2016-06-16 09:17:49 -07003345 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003346 changes = [Changelist(branchref=b, auth_config=auth_config)
3347 for b in branches.splitlines()]
3348 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3349 statuses = get_cl_statuses(changes,
3350 fine_grained=True,
3351 max_processes=options.maxjobs)
3352 proposal = [(cl.GetBranch(),
3353 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3354 for cl, status in statuses
3355 if status == 'closed']
3356 proposal.sort()
3357
3358 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003359 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003360 return 0
3361
3362 current_branch = GetCurrentBranch()
3363
vapiera7fbd5a2016-06-16 09:17:49 -07003364 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003365 if options.notags:
3366 for next_item in proposal:
3367 print(' ' + next_item[0])
3368 else:
3369 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3370 for next_item in proposal:
3371 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003372
kmarshall9249e012016-08-23 12:02:16 -07003373 # Quit now on precondition failure or if instructed by the user, either
3374 # via an interactive prompt or by command line flags.
3375 if options.dry_run:
3376 print('\nNo changes were made (dry run).\n')
3377 return 0
3378 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003379 print('You are currently on a branch \'%s\' which is associated with a '
3380 'closed codereview issue, so archive cannot proceed. Please '
3381 'checkout another branch and run this command again.' %
3382 current_branch)
3383 return 1
kmarshall9249e012016-08-23 12:02:16 -07003384 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003385 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3386 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003387 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003388 return 1
3389
3390 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003391 if not options.notags:
3392 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003393 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003394
vapiera7fbd5a2016-06-16 09:17:49 -07003395 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003396
3397 return 0
3398
3399
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003400def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003401 """Show status of changelists.
3402
3403 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003404 - Red not sent for review or broken
3405 - Blue waiting for review
3406 - Yellow waiting for you to reply to review
3407 - Green LGTM'ed
3408 - Magenta in the commit queue
3409 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003410
3411 Also see 'git cl comments'.
3412 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003413 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003414 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003415 parser.add_option('-f', '--fast', action='store_true',
3416 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003417 parser.add_option(
3418 '-j', '--maxjobs', action='store', type=int,
3419 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003420
3421 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003422 _add_codereview_issue_select_options(
3423 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003424 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003425 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003426 if args:
3427 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003428 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003429
iannuccie53c9352016-08-17 14:40:40 -07003430 if options.issue is not None and not options.field:
3431 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003432
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003433 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003434 cl = Changelist(auth_config=auth_config, issue=options.issue,
3435 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003436 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003437 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003438 elif options.field == 'id':
3439 issueid = cl.GetIssue()
3440 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003441 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003442 elif options.field == 'patch':
3443 patchset = cl.GetPatchset()
3444 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003445 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003446 elif options.field == 'status':
3447 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003448 elif options.field == 'url':
3449 url = cl.GetIssueURL()
3450 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003451 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003452 return 0
3453
3454 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3455 if not branches:
3456 print('No local branch found.')
3457 return 0
3458
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003459 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003460 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003461 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003462 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003463 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003464 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003465 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003466
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003467 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003468 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3469 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3470 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003471 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003472 c, status = output.next()
3473 branch_statuses[c.GetBranch()] = status
3474 status = branch_statuses.pop(branch)
3475 url = cl.GetIssueURL()
3476 if url and (not status or status == 'error'):
3477 # The issue probably doesn't exist anymore.
3478 url += ' (broken)'
3479
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003480 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003481 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003482 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003483 color = ''
3484 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003485 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003486 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003487 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003488 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003489
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003490 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003491 print()
3492 print('Current branch:',)
3493 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003494 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003495 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003496 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003497 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003498 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003499 print('Issue description:')
3500 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003501 return 0
3502
3503
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003504def colorize_CMDstatus_doc():
3505 """To be called once in main() to add colors to git cl status help."""
3506 colors = [i for i in dir(Fore) if i[0].isupper()]
3507
3508 def colorize_line(line):
3509 for color in colors:
3510 if color in line.upper():
3511 # Extract whitespaces first and the leading '-'.
3512 indent = len(line) - len(line.lstrip(' ')) + 1
3513 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3514 return line
3515
3516 lines = CMDstatus.__doc__.splitlines()
3517 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3518
3519
phajdan.jre328cf92016-08-22 04:12:17 -07003520def write_json(path, contents):
3521 with open(path, 'w') as f:
3522 json.dump(contents, f)
3523
3524
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003525@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003526def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003527 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003528
3529 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003530 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003531 parser.add_option('-r', '--reverse', action='store_true',
3532 help='Lookup the branch(es) for the specified issues. If '
3533 'no issues are specified, all branches with mapped '
3534 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003535 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003536 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003537 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003538 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003539
dnj@chromium.org406c4402015-03-03 17:22:28 +00003540 if options.reverse:
3541 branches = RunGit(['for-each-ref', 'refs/heads',
3542 '--format=%(refname:short)']).splitlines()
3543
3544 # Reverse issue lookup.
3545 issue_branch_map = {}
3546 for branch in branches:
3547 cl = Changelist(branchref=branch)
3548 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3549 if not args:
3550 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003551 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003552 for issue in args:
3553 if not issue:
3554 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003555 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003556 print('Branch for issue number %s: %s' % (
3557 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003558 if options.json:
3559 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003560 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003561 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003562 if len(args) > 0:
3563 try:
3564 issue = int(args[0])
3565 except ValueError:
3566 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003567 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003568 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003569 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003570 if options.json:
3571 write_json(options.json, {
3572 'issue': cl.GetIssue(),
3573 'issue_url': cl.GetIssueURL(),
3574 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003575 return 0
3576
3577
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003578def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003579 """Shows or posts review comments for any changelist."""
3580 parser.add_option('-a', '--add-comment', dest='comment',
3581 help='comment to add to an issue')
3582 parser.add_option('-i', dest='issue',
3583 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003584 parser.add_option('-j', '--json-file',
3585 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003586 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003587 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003588 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003589
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003590 issue = None
3591 if options.issue:
3592 try:
3593 issue = int(options.issue)
3594 except ValueError:
3595 DieWithError('A review issue id is expected to be a number')
3596
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003597 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003598
3599 if options.comment:
3600 cl.AddComment(options.comment)
3601 return 0
3602
3603 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003604 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003605 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003606 summary.append({
3607 'date': message['date'],
3608 'lgtm': False,
3609 'message': message['text'],
3610 'not_lgtm': False,
3611 'sender': message['sender'],
3612 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003613 if message['disapproval']:
3614 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003615 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003616 elif message['approval']:
3617 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003618 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003619 elif message['sender'] == data['owner_email']:
3620 color = Fore.MAGENTA
3621 else:
3622 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003623 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003624 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003625 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003626 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003627 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003628 if options.json_file:
3629 with open(options.json_file, 'wb') as f:
3630 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003631 return 0
3632
3633
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003634@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003635def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003636 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003637 parser.add_option('-d', '--display', action='store_true',
3638 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003639 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07003640 help='New description to set for this issue (- for stdin, '
3641 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07003642 parser.add_option('-f', '--force', action='store_true',
3643 help='Delete any unpublished Gerrit edits for this issue '
3644 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003645
3646 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003647 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003648 options, args = parser.parse_args(args)
3649 _process_codereview_select_options(parser, options)
3650
3651 target_issue = None
3652 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07003653 target_issue = ParseIssueNumberArgument(args[0])
3654 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003655 parser.print_help()
3656 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003657
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003658 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003659
martiniss6eda05f2016-06-30 10:18:35 -07003660 kwargs = {
3661 'auth_config': auth_config,
3662 'codereview': options.forced_codereview,
3663 }
3664 if target_issue:
3665 kwargs['issue'] = target_issue.issue
3666 if options.forced_codereview == 'rietveld':
3667 kwargs['rietveld_server'] = target_issue.hostname
3668
3669 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003670
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003671 if not cl.GetIssue():
3672 DieWithError('This branch has no associated changelist.')
3673 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003674
smut@google.com34fb6b12015-07-13 20:03:26 +00003675 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003676 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003677 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003678
3679 if options.new_description:
3680 text = options.new_description
3681 if text == '-':
3682 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07003683 elif text == '+':
3684 base_branch = cl.GetCommonAncestorWithUpstream()
3685 change = cl.GetChange(base_branch, None, local_description=True)
3686 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003687
3688 description.set_description(text)
3689 else:
3690 description.prompt()
3691
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003692 if cl.GetDescription() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07003693 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003694 return 0
3695
3696
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003697def CreateDescriptionFromLog(args):
3698 """Pulls out the commit log to use as a base for the CL description."""
3699 log_args = []
3700 if len(args) == 1 and not args[0].endswith('.'):
3701 log_args = [args[0] + '..']
3702 elif len(args) == 1 and args[0].endswith('...'):
3703 log_args = [args[0][:-1]]
3704 elif len(args) == 2:
3705 log_args = [args[0] + '..' + args[1]]
3706 else:
3707 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003708 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003709
3710
thestig@chromium.org44202a22014-03-11 19:22:18 +00003711def CMDlint(parser, args):
3712 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003713 parser.add_option('--filter', action='append', metavar='-x,+y',
3714 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003715 auth.add_auth_options(parser)
3716 options, args = parser.parse_args(args)
3717 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003718
3719 # Access to a protected member _XX of a client class
3720 # pylint: disable=W0212
3721 try:
3722 import cpplint
3723 import cpplint_chromium
3724 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003725 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003726 return 1
3727
3728 # Change the current working directory before calling lint so that it
3729 # shows the correct base.
3730 previous_cwd = os.getcwd()
3731 os.chdir(settings.GetRoot())
3732 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003733 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003734 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3735 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003736 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003737 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003738 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003739
3740 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003741 command = args + files
3742 if options.filter:
3743 command = ['--filter=' + ','.join(options.filter)] + command
3744 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003745
3746 white_regex = re.compile(settings.GetLintRegex())
3747 black_regex = re.compile(settings.GetLintIgnoreRegex())
3748 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3749 for filename in filenames:
3750 if white_regex.match(filename):
3751 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07003752 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003753 else:
3754 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3755 extra_check_functions)
3756 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003757 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003758 finally:
3759 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07003760 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003761 if cpplint._cpplint_state.error_count != 0:
3762 return 1
3763 return 0
3764
3765
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003766def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003767 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003768 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003769 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003770 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003771 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003772 auth.add_auth_options(parser)
3773 options, args = parser.parse_args(args)
3774 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003775
sbc@chromium.org71437c02015-04-09 19:29:40 +00003776 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07003777 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003778 return 1
3779
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003780 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003781 if args:
3782 base_branch = args[0]
3783 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003784 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003785 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003786
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003787 cl.RunHook(
3788 committing=not options.upload,
3789 may_prompt=False,
3790 verbose=options.verbose,
3791 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003792 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003793
3794
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003795def GenerateGerritChangeId(message):
3796 """Returns Ixxxxxx...xxx change id.
3797
3798 Works the same way as
3799 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3800 but can be called on demand on all platforms.
3801
3802 The basic idea is to generate git hash of a state of the tree, original commit
3803 message, author/committer info and timestamps.
3804 """
3805 lines = []
3806 tree_hash = RunGitSilent(['write-tree'])
3807 lines.append('tree %s' % tree_hash.strip())
3808 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3809 if code == 0:
3810 lines.append('parent %s' % parent.strip())
3811 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3812 lines.append('author %s' % author.strip())
3813 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3814 lines.append('committer %s' % committer.strip())
3815 lines.append('')
3816 # Note: Gerrit's commit-hook actually cleans message of some lines and
3817 # whitespace. This code is not doing this, but it clearly won't decrease
3818 # entropy.
3819 lines.append(message)
3820 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3821 stdin='\n'.join(lines))
3822 return 'I%s' % change_hash.strip()
3823
3824
wittman@chromium.org455dc922015-01-26 20:15:50 +00003825def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3826 """Computes the remote branch ref to use for the CL.
3827
3828 Args:
3829 remote (str): The git remote for the CL.
3830 remote_branch (str): The git remote branch for the CL.
3831 target_branch (str): The target branch specified by the user.
3832 pending_prefix (str): The pending prefix from the settings.
3833 """
3834 if not (remote and remote_branch):
3835 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003836
wittman@chromium.org455dc922015-01-26 20:15:50 +00003837 if target_branch:
3838 # Cannonicalize branch references to the equivalent local full symbolic
3839 # refs, which are then translated into the remote full symbolic refs
3840 # below.
3841 if '/' not in target_branch:
3842 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3843 else:
3844 prefix_replacements = (
3845 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3846 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3847 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3848 )
3849 match = None
3850 for regex, replacement in prefix_replacements:
3851 match = re.search(regex, target_branch)
3852 if match:
3853 remote_branch = target_branch.replace(match.group(0), replacement)
3854 break
3855 if not match:
3856 # This is a branch path but not one we recognize; use as-is.
3857 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003858 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3859 # Handle the refs that need to land in different refs.
3860 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003861
wittman@chromium.org455dc922015-01-26 20:15:50 +00003862 # Create the true path to the remote branch.
3863 # Does the following translation:
3864 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3865 # * refs/remotes/origin/master -> refs/heads/master
3866 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3867 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3868 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3869 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3870 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3871 'refs/heads/')
3872 elif remote_branch.startswith('refs/remotes/branch-heads'):
3873 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3874 # If a pending prefix exists then replace refs/ with it.
3875 if pending_prefix:
3876 remote_branch = remote_branch.replace('refs/', pending_prefix)
3877 return remote_branch
3878
3879
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003880def cleanup_list(l):
3881 """Fixes a list so that comma separated items are put as individual items.
3882
3883 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3884 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3885 """
3886 items = sum((i.split(',') for i in l), [])
3887 stripped_items = (i.strip() for i in items)
3888 return sorted(filter(None, stripped_items))
3889
3890
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003891@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003892def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003893 """Uploads the current changelist to codereview.
3894
3895 Can skip dependency patchset uploads for a branch by running:
3896 git config branch.branch_name.skip-deps-uploads True
3897 To unset run:
3898 git config --unset branch.branch_name.skip-deps-uploads
3899 Can also set the above globally by using the --global flag.
3900 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003901 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3902 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003903 parser.add_option('--bypass-watchlists', action='store_true',
3904 dest='bypass_watchlists',
3905 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003906 parser.add_option('-f', action='store_true', dest='force',
3907 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003908 parser.add_option('-m', dest='message', help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07003909 parser.add_option('-b', '--bug',
3910 help='pre-populate the bug number(s) for this issue. '
3911 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07003912 parser.add_option('--message-file', dest='message_file',
3913 help='file which contains message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003914 parser.add_option('-t', dest='title',
3915 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003916 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003917 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003918 help='reviewer email addresses')
3919 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003920 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003921 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003922 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003923 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003924 parser.add_option('--emulate_svn_auto_props',
3925 '--emulate-svn-auto-props',
3926 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003927 dest="emulate_svn_auto_props",
3928 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003929 parser.add_option('-c', '--use-commit-queue', action='store_true',
3930 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003931 parser.add_option('--private', action='store_true',
3932 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003933 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003934 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003935 metavar='TARGET',
3936 help='Apply CL to remote ref TARGET. ' +
3937 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003938 parser.add_option('--squash', action='store_true',
3939 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003940 parser.add_option('--no-squash', action='store_true',
3941 help='Don\'t squash multiple commits into one ' +
3942 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07003943 parser.add_option('--topic', default=None,
3944 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003945 parser.add_option('--email', default=None,
3946 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003947 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3948 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003949 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3950 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003951 help='Send the patchset to do a CQ dry run right after '
3952 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003953 parser.add_option('--dependencies', action='store_true',
3954 help='Uploads CLs of all the local branches that depend on '
3955 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003956
rmistry@google.com2dd99862015-06-22 12:22:18 +00003957 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003958 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003959 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003960 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003961 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003962 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003963 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003964
sbc@chromium.org71437c02015-04-09 19:29:40 +00003965 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003966 return 1
3967
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003968 options.reviewers = cleanup_list(options.reviewers)
3969 options.cc = cleanup_list(options.cc)
3970
tandriib80458a2016-06-23 12:20:07 -07003971 if options.message_file:
3972 if options.message:
3973 parser.error('only one of --message and --message-file allowed.')
3974 options.message = gclient_utils.FileRead(options.message_file)
3975 options.message_file = None
3976
tandrii4d0545a2016-07-06 03:56:49 -07003977 if options.cq_dry_run and options.use_commit_queue:
3978 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
3979
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003980 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3981 settings.GetIsGerrit()
3982
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003983 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00003984 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003985
3986
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003987def IsSubmoduleMergeCommit(ref):
3988 # When submodules are added to the repo, we expect there to be a single
3989 # non-git-svn merge commit at remote HEAD with a signature comment.
3990 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003991 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003992 return RunGit(cmd) != ''
3993
3994
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003995def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003996 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003997
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003998 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3999 upstream and closes the issue automatically and atomically.
4000
4001 Otherwise (in case of Rietveld):
4002 Squashes branch into a single commit.
4003 Updates changelog with metadata (e.g. pointer to review).
4004 Pushes/dcommits the code upstream.
4005 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004006 """
4007 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4008 help='bypass upload presubmit hook')
4009 parser.add_option('-m', dest='message',
4010 help="override review description")
4011 parser.add_option('-f', action='store_true', dest='force',
4012 help="force yes to questions (don't prompt)")
4013 parser.add_option('-c', dest='contributor',
4014 help="external contributor for patch (appended to " +
4015 "description and used as author for git). Should be " +
4016 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004017 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004018 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004019 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004020 auth_config = auth.extract_auth_config_from_options(options)
4021
4022 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004023
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004024 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4025 if cl.IsGerrit():
4026 if options.message:
4027 # This could be implemented, but it requires sending a new patch to
4028 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4029 # Besides, Gerrit has the ability to change the commit message on submit
4030 # automatically, thus there is no need to support this option (so far?).
4031 parser.error('-m MESSAGE option is not supported for Gerrit.')
4032 if options.contributor:
4033 parser.error(
4034 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4035 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4036 'the contributor\'s "name <email>". If you can\'t upload such a '
4037 'commit for review, contact your repository admin and request'
4038 '"Forge-Author" permission.')
tandrii73449b02016-09-14 06:27:24 -07004039 if not cl.GetIssue():
4040 DieWithError('You must upload the issue first to Gerrit.\n'
4041 ' If you would rather have `git cl land` upload '
4042 'automatically for you, see http://crbug.com/642759')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004043 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4044 options.verbose)
4045
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004046 current = cl.GetBranch()
4047 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4048 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07004049 print()
4050 print('Attempting to push branch %r into another local branch!' % current)
4051 print()
4052 print('Either reparent this branch on top of origin/master:')
4053 print(' git reparent-branch --root')
4054 print()
4055 print('OR run `git rebase-update` if you think the parent branch is ')
4056 print('already committed.')
4057 print()
4058 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004059 return 1
4060
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004061 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004062 # Default to merging against our best guess of the upstream branch.
4063 args = [cl.GetUpstreamBranch()]
4064
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004065 if options.contributor:
4066 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07004067 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004068 return 1
4069
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004070 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004071 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004072
sbc@chromium.org71437c02015-04-09 19:29:40 +00004073 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004074 return 1
4075
4076 # This rev-list syntax means "show all commits not in my branch that
4077 # are in base_branch".
4078 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4079 base_branch]).splitlines()
4080 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004081 print('Base branch "%s" has %d commits '
4082 'not in this branch.' % (base_branch, len(upstream_commits)))
4083 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004084 return 1
4085
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004086 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004087 svn_head = None
4088 if cmd == 'dcommit' or base_has_submodules:
4089 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
4090 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004091
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004092 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004093 # If the base_head is a submodule merge commit, the first parent of the
4094 # base_head should be a git-svn commit, which is what we're interested in.
4095 base_svn_head = base_branch
4096 if base_has_submodules:
4097 base_svn_head += '^1'
4098
4099 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004100 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004101 print('This branch has %d additional commits not upstreamed yet.'
4102 % len(extra_commits.splitlines()))
4103 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
4104 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004105 return 1
4106
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004107 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004108 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004109 author = None
4110 if options.contributor:
4111 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004112 hook_results = cl.RunHook(
4113 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004114 may_prompt=not options.force,
4115 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004116 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004117 if not hook_results.should_continue():
4118 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004119
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004120 # Check the tree status if the tree status URL is set.
4121 status = GetTreeStatus()
4122 if 'closed' == status:
4123 print('The tree is closed. Please wait for it to reopen. Use '
4124 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4125 return 1
4126 elif 'unknown' == status:
4127 print('Unable to determine tree status. Please verify manually and '
4128 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4129 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004130
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004131 change_desc = ChangeDescription(options.message)
4132 if not change_desc.description and cl.GetIssue():
4133 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004134
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004135 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00004136 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004137 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00004138 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004139 print('No description set.')
4140 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00004141 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004142
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004143 # Keep a separate copy for the commit message, because the commit message
4144 # contains the link to the Rietveld issue, while the Rietveld message contains
4145 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004146 # Keep a separate copy for the commit message.
4147 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00004148 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004149
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004150 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00004151 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00004152 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00004153 # after it. Add a period on a new line to circumvent this. Also add a space
4154 # before the period to make sure that Gitiles continues to correctly resolve
4155 # the URL.
4156 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004157 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004158 commit_desc.append_footer('Patch from %s.' % options.contributor)
4159
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00004160 print('Description:')
4161 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004162
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004163 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004164 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00004165 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004166
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004167 # We want to squash all this branch's commits into one commit with the proper
4168 # description. We do this by doing a "reset --soft" to the base branch (which
4169 # keeps the working copy the same), then dcommitting that. If origin/master
4170 # has a submodule merge commit, we'll also need to cherry-pick the squashed
4171 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004172 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004173 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4174 # Delete the branches if they exist.
4175 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4176 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4177 result = RunGitWithCode(showref_cmd)
4178 if result[0] == 0:
4179 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004180
4181 # We might be in a directory that's present in this branch but not in the
4182 # trunk. Move up to the top of the tree so that git commands that expect a
4183 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004184 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004185 if rel_base_path:
4186 os.chdir(rel_base_path)
4187
4188 # Stuff our change into the merge branch.
4189 # We wrap in a try...finally block so if anything goes wrong,
4190 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004191 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004192 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004193 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004194 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004195 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00004196 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004197 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004198 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004199 RunGit(
4200 [
4201 'commit', '--author', options.contributor,
4202 '-m', commit_desc.description,
4203 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004204 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004205 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004206 if base_has_submodules:
4207 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
4208 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
4209 RunGit(['checkout', CHERRY_PICK_BRANCH])
4210 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004211 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004212 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004213 mirror = settings.GetGitMirror(remote)
4214 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004215 pending_prefix = settings.GetPendingRefPrefix()
4216 if not pending_prefix or branch.startswith(pending_prefix):
4217 # If not using refs/pending/heads/* at all, or target ref is already set
4218 # to pending, then push to the target ref directly.
4219 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004220 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004221 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004222 else:
4223 # Cherry-pick the change on top of pending ref and then push it.
4224 assert branch.startswith('refs/'), branch
4225 assert pending_prefix[-1] == '/', pending_prefix
4226 pending_ref = pending_prefix + branch[len('refs/'):]
tandriibf429402016-09-14 07:09:12 -07004227 retcode, output = PushToGitPending(pushurl, pending_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004228 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004229 if retcode == 0:
4230 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004231 else:
4232 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004233 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00004234 'svn', 'dcommit',
4235 '-C%s' % options.similarity,
4236 '--no-rebase', '--rmdir',
4237 ]
4238 if settings.GetForceHttpsCommitUrl():
4239 # Allow forcing https commit URLs for some projects that don't allow
4240 # committing to http URLs (like Google Code).
4241 remote_url = cl.GetGitSvnRemoteUrl()
4242 if urlparse.urlparse(remote_url).scheme == 'http':
4243 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004244 cmd_args.append('--commit-url=%s' % remote_url)
4245 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004246 if 'Committed r' in output:
4247 revision = re.match(
4248 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4249 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004250 finally:
4251 # And then swap back to the original branch and clean up.
4252 RunGit(['checkout', '-q', cl.GetBranch()])
4253 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004254 if base_has_submodules:
4255 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004256
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004257 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004258 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004259 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004260
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004261 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004262 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004263 try:
4264 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4265 # We set pushed_to_pending to False, since it made it all the way to the
4266 # real ref.
4267 pushed_to_pending = False
4268 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004269 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004270
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004271 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004272 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004273 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004274 if not to_pending:
4275 if viewvc_url and revision:
4276 change_desc.append_footer(
4277 'Committed: %s%s' % (viewvc_url, revision))
4278 elif revision:
4279 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004280 print('Closing issue '
4281 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004282 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004283 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004284 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004285 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004286 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004287 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004288 if options.bypass_hooks:
4289 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4290 else:
4291 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004292 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004293
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004294 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004295 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004296 print('The commit is in the pending queue (%s).' % pending_ref)
4297 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4298 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004299
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004300 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4301 if os.path.isfile(hook):
4302 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004303
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004304 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004305
4306
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004307def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004308 print()
4309 print('Waiting for commit to be landed on %s...' % real_ref)
4310 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004311 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4312 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004313 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004314
4315 loop = 0
4316 while True:
4317 sys.stdout.write('fetching (%d)... \r' % loop)
4318 sys.stdout.flush()
4319 loop += 1
4320
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004321 if mirror:
4322 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004323 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4324 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4325 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4326 for commit in commits.splitlines():
4327 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004328 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004329 return commit
4330
4331 current_rev = to_rev
4332
4333
tandriibf429402016-09-14 07:09:12 -07004334def PushToGitPending(remote, pending_ref):
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004335 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4336
4337 Returns:
4338 (retcode of last operation, output log of last operation).
4339 """
4340 assert pending_ref.startswith('refs/'), pending_ref
4341 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4342 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4343 code = 0
4344 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004345 max_attempts = 3
4346 attempts_left = max_attempts
4347 while attempts_left:
4348 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004349 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004350 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004351
4352 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004353 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004354 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004355 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004356 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004357 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004358 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004359 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004360 continue
4361
4362 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004363 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004364 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004365 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004366 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004367 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4368 'the following files have merge conflicts:' % pending_ref)
4369 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4370 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004371 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004372 return code, out
4373
4374 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004375 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004376 code, out = RunGitWithCode(
4377 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4378 if code == 0:
4379 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004380 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004381 return code, out
4382
vapiera7fbd5a2016-06-16 09:17:49 -07004383 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004384 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004385 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004386 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004387 print('Fatal push error. Make sure your .netrc credentials and git '
4388 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004389 return code, out
4390
vapiera7fbd5a2016-06-16 09:17:49 -07004391 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004392 return code, out
4393
4394
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004395def IsFatalPushFailure(push_stdout):
4396 """True if retrying push won't help."""
4397 return '(prohibited by Gerrit)' in push_stdout
4398
4399
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004400@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004401def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004402 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004403 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004404 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004405 # If it looks like previous commits were mirrored with git-svn.
agable3b9a5bb2016-09-22 11:32:08 -07004406 message = """This repository appears to be a git-svn mirror, but we
4407don't support git-svn mirrors anymore."""
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004408 else:
4409 message = """This doesn't appear to be an SVN repository.
4410If your project has a true, writeable git repository, you probably want to run
4411'git cl land' instead.
4412If your project has a git mirror of an upstream SVN master, you probably need
4413to run 'git svn init'.
4414
4415Using the wrong command might cause your commit to appear to succeed, and the
4416review to be closed, without actually landing upstream. If you choose to
4417proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004418 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004419 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
tandrii3bb82ff2016-06-17 07:36:36 -07004420 print('WARNING: chrome infrastructure is migrating SVN repos to Git.\n'
4421 'Please let us know of this project you are committing to:'
4422 ' http://crbug.com/600451')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004423 return SendUpstream(parser, args, 'dcommit')
4424
4425
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004426@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004427def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004428 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004429 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004430 print('This appears to be an SVN repository.')
4431 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004432 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004433 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004434 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004435
4436
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004437@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004438def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004439 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004440 parser.add_option('-b', dest='newbranch',
4441 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004442 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004443 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004444 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4445 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004446 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004447 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004448 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004449 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004450 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004451 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004452
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004453
4454 group = optparse.OptionGroup(
4455 parser,
4456 'Options for continuing work on the current issue uploaded from a '
4457 'different clone (e.g. different machine). Must be used independently '
4458 'from the other options. No issue number should be specified, and the '
4459 'branch must have an issue number associated with it')
4460 group.add_option('--reapply', action='store_true', dest='reapply',
4461 help='Reset the branch and reapply the issue.\n'
4462 'CAUTION: This will undo any local changes in this '
4463 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004464
4465 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004466 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004467 parser.add_option_group(group)
4468
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004469 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004470 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004471 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004472 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004473 auth_config = auth.extract_auth_config_from_options(options)
4474
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004475
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004476 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004477 if options.newbranch:
4478 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004479 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004480 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004481
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004482 cl = Changelist(auth_config=auth_config,
4483 codereview=options.forced_codereview)
4484 if not cl.GetIssue():
4485 parser.error('current branch must have an associated issue')
4486
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004487 upstream = cl.GetUpstreamBranch()
4488 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004489 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004490
4491 RunGit(['reset', '--hard', upstream])
4492 if options.pull:
4493 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004494
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004495 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4496 options.directory)
4497
4498 if len(args) != 1 or not args[0]:
4499 parser.error('Must specify issue number or url')
4500
4501 # We don't want uncommitted changes mixed up with the patch.
4502 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004503 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004504
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004505 if options.newbranch:
4506 if options.force:
4507 RunGit(['branch', '-D', options.newbranch],
4508 stderr=subprocess2.PIPE, error_ok=True)
4509 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004510 elif not GetCurrentBranch():
4511 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004512
4513 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4514
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004515 if cl.IsGerrit():
4516 if options.reject:
4517 parser.error('--reject is not supported with Gerrit codereview.')
4518 if options.nocommit:
4519 parser.error('--nocommit is not supported with Gerrit codereview.')
4520 if options.directory:
4521 parser.error('--directory is not supported with Gerrit codereview.')
4522
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004523 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004524 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004525
4526
4527def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004528 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004529 # Provide a wrapper for git svn rebase to help avoid accidental
4530 # git svn dcommit.
4531 # It's the only command that doesn't use parser at all since we just defer
4532 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004533
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004534 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004535
4536
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004537def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004538 """Fetches the tree status and returns either 'open', 'closed',
4539 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004540 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004541 if url:
4542 status = urllib2.urlopen(url).read().lower()
4543 if status.find('closed') != -1 or status == '0':
4544 return 'closed'
4545 elif status.find('open') != -1 or status == '1':
4546 return 'open'
4547 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004548 return 'unset'
4549
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004550
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004551def GetTreeStatusReason():
4552 """Fetches the tree status from a json url and returns the message
4553 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004554 url = settings.GetTreeStatusUrl()
4555 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004556 connection = urllib2.urlopen(json_url)
4557 status = json.loads(connection.read())
4558 connection.close()
4559 return status['message']
4560
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004561
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004562def GetBuilderMaster(bot_list):
4563 """For a given builder, fetch the master from AE if available."""
4564 map_url = 'https://builders-map.appspot.com/'
4565 try:
4566 master_map = json.load(urllib2.urlopen(map_url))
4567 except urllib2.URLError as e:
4568 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4569 (map_url, e))
4570 except ValueError as e:
4571 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4572 if not master_map:
4573 return None, 'Failed to build master map.'
4574
4575 result_master = ''
4576 for bot in bot_list:
4577 builder = bot.split(':', 1)[0]
4578 master_list = master_map.get(builder, [])
4579 if not master_list:
4580 return None, ('No matching master for builder %s.' % builder)
4581 elif len(master_list) > 1:
4582 return None, ('The builder name %s exists in multiple masters %s.' %
4583 (builder, master_list))
4584 else:
4585 cur_master = master_list[0]
4586 if not result_master:
4587 result_master = cur_master
4588 elif result_master != cur_master:
4589 return None, 'The builders do not belong to the same master.'
4590 return result_master, None
4591
4592
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004593def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004594 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004595 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004596 status = GetTreeStatus()
4597 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004598 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004599 return 2
4600
vapiera7fbd5a2016-06-16 09:17:49 -07004601 print('The tree is %s' % status)
4602 print()
4603 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004604 if status != 'open':
4605 return 1
4606 return 0
4607
4608
maruel@chromium.org15192402012-09-06 12:38:29 +00004609def CMDtry(parser, args):
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004610 """Triggers try jobs through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00004611 group = optparse.OptionGroup(parser, "Try job options")
4612 group.add_option(
4613 "-b", "--bot", action="append",
4614 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
4615 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004616 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00004617 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004618 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00004619 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004620 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00004621 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004622 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004623 "-r", "--revision",
4624 help="Revision to use for the try job; default: the "
4625 "revision will be determined by the try server; see "
4626 "its waterfall for more info")
4627 group.add_option(
4628 "-c", "--clobber", action="store_true", default=False,
4629 help="Force a clobber before building; e.g. don't do an "
4630 "incremental build")
4631 group.add_option(
4632 "--project",
4633 help="Override which project to use. Projects are defined "
4634 "server-side to define what default bot set to use")
4635 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004636 "-p", "--property", dest="properties", action="append", default=[],
4637 help="Specify generic properties in the form -p key1=value1 -p "
4638 "key2=value2 etc (buildbucket only). The value will be treated as "
4639 "json if decodable, or as string otherwise.")
4640 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004641 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004642 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004643 "--use-rietveld", action="store_true", default=False,
4644 help="Use Rietveld to trigger try jobs.")
4645 group.add_option(
4646 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4647 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004648 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004649 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004650 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004651 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004652
machenbach@chromium.org45453142015-09-15 08:45:22 +00004653 if options.use_rietveld and options.properties:
4654 parser.error('Properties can only be specified with buildbucket')
4655
4656 # Make sure that all properties are prop=value pairs.
4657 bad_params = [x for x in options.properties if '=' not in x]
4658 if bad_params:
4659 parser.error('Got properties with missing "=": %s' % bad_params)
4660
maruel@chromium.org15192402012-09-06 12:38:29 +00004661 if args:
4662 parser.error('Unknown arguments: %s' % args)
4663
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004664 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004665 if not cl.GetIssue():
4666 parser.error('Need to upload first')
4667
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004668 if cl.IsGerrit():
4669 parser.error(
4670 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4671 'If your project has Commit Queue, dry run is a workaround:\n'
4672 ' git cl set-commit --dry-run')
4673 # Code below assumes Rietveld issue.
4674 # TODO(tandrii): actually implement for Gerrit http://crbug.com/599931.
4675
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004676 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004677 if props.get('closed'):
qyearsleyeab3c042016-08-24 09:18:28 -07004678 parser.error('Cannot send try jobs for a closed CL')
agable@chromium.org787e3062014-08-20 16:31:19 +00004679
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004680 if props.get('private'):
qyearsleyeab3c042016-08-24 09:18:28 -07004681 parser.error('Cannot use try bots with private issue')
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004682
maruel@chromium.org15192402012-09-06 12:38:29 +00004683 if not options.name:
4684 options.name = cl.GetBranch()
4685
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004686 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004687 options.master, err_msg = GetBuilderMaster(options.bot)
4688 if err_msg:
4689 parser.error('Tryserver master cannot be found because: %s\n'
4690 'Please manually specify the tryserver master'
4691 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004692
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004693 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004694 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004695 if not options.bot:
4696 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004697
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004698 # Get try masters from PRESUBMIT.py files.
4699 masters = presubmit_support.DoGetTryMasters(
4700 change,
4701 change.LocalPaths(),
4702 settings.GetRoot(),
4703 None,
4704 None,
4705 options.verbose,
4706 sys.stdout)
4707 if masters:
4708 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004709
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004710 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4711 options.bot = presubmit_support.DoGetTrySlaves(
4712 change,
4713 change.LocalPaths(),
4714 settings.GetRoot(),
4715 None,
4716 None,
4717 options.verbose,
4718 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004719
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004720 if not options.bot:
tandrii9de9ec62016-07-13 03:01:59 -07004721 return {}
maruel@chromium.org15192402012-09-06 12:38:29 +00004722
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004723 builders_and_tests = {}
4724 # TODO(machenbach): The old style command-line options don't support
4725 # multiple try masters yet.
4726 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4727 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4728
4729 for bot in old_style:
4730 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004731 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004732 elif ',' in bot:
4733 parser.error('Specify one bot per --bot flag')
4734 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004735 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004736
4737 for bot, tests in new_style:
4738 builders_and_tests.setdefault(bot, []).extend(tests)
4739
4740 # Return a master map with one master to be backwards compatible. The
4741 # master name defaults to an empty string, which will cause the master
4742 # not to be set on rietveld (deprecated).
4743 return {options.master: builders_and_tests}
4744
4745 masters = GetMasterMap()
tandrii9de9ec62016-07-13 03:01:59 -07004746 if not masters:
4747 # Default to triggering Dry Run (see http://crbug.com/625697).
4748 if options.verbose:
4749 print('git cl try with no bots now defaults to CQ Dry Run.')
4750 try:
4751 cl.SetCQState(_CQState.DRY_RUN)
4752 print('scheduled CQ Dry Run on %s' % cl.GetIssueURL())
4753 return 0
4754 except KeyboardInterrupt:
4755 raise
4756 except:
4757 print('WARNING: failed to trigger CQ Dry Run.\n'
4758 'Either:\n'
4759 ' * your project has no CQ\n'
4760 ' * you don\'t have permission to trigger Dry Run\n'
4761 ' * bug in this code (see stack trace below).\n'
4762 'Consider specifying which bots to trigger manually '
4763 'or asking your project owners for permissions '
4764 'or contacting Chrome Infrastructure team at '
4765 'https://www.chromium.org/infra\n\n')
4766 # Still raise exception so that stack trace is printed.
4767 raise
stip@chromium.org43064fd2013-12-18 20:07:44 +00004768
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004769 for builders in masters.itervalues():
4770 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004771 print('ERROR You are trying to send a job to a triggered bot. This type '
4772 'of bot requires an\ninitial job from a parent (usually a builder).'
4773 ' Instead send your job to the parent.\n'
4774 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004775 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004776
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004777 patchset = cl.GetMostRecentPatchset()
4778 if patchset and patchset != cl.GetPatchset():
4779 print(
4780 '\nWARNING Mismatch between local config and server. Did a previous '
4781 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4782 'Continuing using\npatchset %s.\n' % patchset)
nodirabb9b222016-07-29 14:23:20 -07004783 if not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004784 try:
4785 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4786 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004787 print('ERROR: %s' % ex)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004788 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004789 except Exception as e:
4790 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
qyearsleyeab3c042016-08-24 09:18:28 -07004791 print('ERROR: Exception when trying to trigger try jobs: %s\n%s' %
vapiera7fbd5a2016-06-16 09:17:49 -07004792 (e, stacktrace))
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004793 return 1
4794 else:
4795 try:
4796 cl.RpcServer().trigger_distributed_try_jobs(
4797 cl.GetIssue(), patchset, options.name, options.clobber,
4798 options.revision, masters)
4799 except urllib2.HTTPError as e:
4800 if e.code == 404:
4801 print('404 from rietveld; '
4802 'did you mean to use "git try" instead of "git cl try"?')
4803 return 1
4804 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004805
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004806 for (master, builders) in sorted(masters.iteritems()):
4807 if master:
vapiera7fbd5a2016-06-16 09:17:49 -07004808 print('Master: %s' % master)
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004809 length = max(len(builder) for builder in builders)
4810 for builder in sorted(builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004811 print(' %*s: %s' % (length, builder, ','.join(builders[builder])))
maruel@chromium.org15192402012-09-06 12:38:29 +00004812 return 0
4813
4814
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004815def CMDtry_results(parser, args):
4816 group = optparse.OptionGroup(parser, "Try job results options")
4817 group.add_option(
4818 "-p", "--patchset", type=int, help="patchset number if not current.")
4819 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004820 "--print-master", action='store_true', help="print master name as well.")
4821 group.add_option(
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004822 "--color", action='store_true', default=setup_color.IS_TTY,
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004823 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004824 group.add_option(
4825 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4826 help="Host of buildbucket. The default host is %default.")
qyearsley53f48a12016-09-01 10:45:13 -07004827 group.add_option(
4828 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004829 parser.add_option_group(group)
4830 auth.add_auth_options(parser)
4831 options, args = parser.parse_args(args)
4832 if args:
4833 parser.error('Unrecognized args: %s' % ' '.join(args))
4834
4835 auth_config = auth.extract_auth_config_from_options(options)
4836 cl = Changelist(auth_config=auth_config)
4837 if not cl.GetIssue():
4838 parser.error('Need to upload first')
4839
4840 if not options.patchset:
4841 options.patchset = cl.GetMostRecentPatchset()
4842 if options.patchset and options.patchset != cl.GetPatchset():
4843 print(
4844 '\nWARNING Mismatch between local config and server. Did a previous '
4845 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4846 'Continuing using\npatchset %s.\n' % options.patchset)
4847 try:
4848 jobs = fetch_try_jobs(auth_config, cl, options)
4849 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004850 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004851 return 1
4852 except Exception as e:
4853 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
qyearsleyeab3c042016-08-24 09:18:28 -07004854 print('ERROR: Exception when trying to fetch try jobs: %s\n%s' %
vapiera7fbd5a2016-06-16 09:17:49 -07004855 (e, stacktrace))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004856 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004857 if options.json:
4858 write_try_results_json(options.json, jobs)
4859 else:
4860 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004861 return 0
4862
4863
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004864@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004865def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004866 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004867 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004868 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004869 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004870
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004871 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004872 if args:
4873 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004874 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07004875 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004876 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004877 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004878
4879 # Clear configured merge-base, if there is one.
4880 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004881 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004882 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004883 return 0
4884
4885
thestig@chromium.org00858c82013-12-02 23:08:03 +00004886def CMDweb(parser, args):
4887 """Opens the current CL in the web browser."""
4888 _, args = parser.parse_args(args)
4889 if args:
4890 parser.error('Unrecognized args: %s' % ' '.join(args))
4891
4892 issue_url = Changelist().GetIssueURL()
4893 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004894 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004895 return 1
4896
4897 webbrowser.open(issue_url)
4898 return 0
4899
4900
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004901def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004902 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004903 parser.add_option('-d', '--dry-run', action='store_true',
4904 help='trigger in dry run mode')
4905 parser.add_option('-c', '--clear', action='store_true',
4906 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004907 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004908 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004909 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004910 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004911 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004912 if args:
4913 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004914 if options.dry_run and options.clear:
4915 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4916
iannuccie53c9352016-08-17 14:40:40 -07004917 cl = Changelist(auth_config=auth_config, issue=options.issue,
4918 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004919 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07004920 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004921 elif options.dry_run:
4922 state = _CQState.DRY_RUN
4923 else:
4924 state = _CQState.COMMIT
4925 if not cl.GetIssue():
4926 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07004927 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004928 return 0
4929
4930
groby@chromium.org411034a2013-02-26 15:12:01 +00004931def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004932 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07004933 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004934 auth.add_auth_options(parser)
4935 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004936 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004937 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004938 if args:
4939 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07004940 cl = Changelist(auth_config=auth_config, issue=options.issue,
4941 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00004942 # Ensure there actually is an issue to close.
4943 cl.GetDescription()
4944 cl.CloseIssue()
4945 return 0
4946
4947
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004948def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004949 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07004950 parser.add_option(
4951 '--stat',
4952 action='store_true',
4953 dest='stat',
4954 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004955 auth.add_auth_options(parser)
4956 options, args = parser.parse_args(args)
4957 auth_config = auth.extract_auth_config_from_options(options)
4958 if args:
4959 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004960
4961 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004962 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004963 # Staged changes would be committed along with the patch from last
4964 # upload, hence counted toward the "last upload" side in the final
4965 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004966 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004967 return 1
4968
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004969 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004970 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004971 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004972 if not issue:
4973 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004974 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004975 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004976
4977 # Create a new branch based on the merge-base
4978 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004979 # Clear cached branch in cl object, to avoid overwriting original CL branch
4980 # properties.
4981 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004982 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004983 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004984 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004985 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004986 return rtn
4987
wychen@chromium.org06928532015-02-03 02:11:29 +00004988 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004989 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07004990 cmd = ['git', 'diff']
4991 if options.stat:
4992 cmd.append('--stat')
4993 cmd.extend([TMP_BRANCH, branch, '--'])
4994 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004995 finally:
4996 RunGit(['checkout', '-q', branch])
4997 RunGit(['branch', '-D', TMP_BRANCH])
4998
4999 return 0
5000
5001
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005002def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005003 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005004 parser.add_option(
5005 '--no-color',
5006 action='store_true',
5007 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005008 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005009 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005010 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005011
5012 author = RunGit(['config', 'user.email']).strip() or None
5013
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005014 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005015
5016 if args:
5017 if len(args) > 1:
5018 parser.error('Unknown args')
5019 base_branch = args[0]
5020 else:
5021 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005022 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005023
5024 change = cl.GetChange(base_branch, None)
5025 return owners_finder.OwnersFinder(
5026 [f.LocalPath() for f in
5027 cl.GetChange(base_branch, None).AffectedFiles()],
5028 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07005029 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005030 disable_color=options.no_color).run()
5031
5032
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005033def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005034 """Generates a diff command."""
5035 # Generate diff for the current branch's changes.
5036 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
5037 upstream_commit, '--' ]
5038
5039 if args:
5040 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005041 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005042 diff_cmd.append(arg)
5043 else:
5044 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005045
5046 return diff_cmd
5047
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005048def MatchingFileType(file_name, extensions):
5049 """Returns true if the file name ends with one of the given extensions."""
5050 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005051
enne@chromium.org555cfe42014-01-29 18:21:39 +00005052@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005053def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005054 """Runs auto-formatting tools (clang-format etc.) on the diff."""
zengsterbf470142016-07-07 16:43:00 -07005055 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005056 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005057 parser.add_option('--full', action='store_true',
5058 help='Reformat the full content of all touched files')
5059 parser.add_option('--dry-run', action='store_true',
5060 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005061 parser.add_option('--python', action='store_true',
5062 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005063 parser.add_option('--diff', action='store_true',
5064 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005065 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005066
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005067 # git diff generates paths against the root of the repository. Change
5068 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005069 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005070 if rel_base_path:
5071 os.chdir(rel_base_path)
5072
digit@chromium.org29e47272013-05-17 17:01:46 +00005073 # Grab the merge-base commit, i.e. the upstream commit of the current
5074 # branch when it was created or the last time it was rebased. This is
5075 # to cover the case where the user may have called "git fetch origin",
5076 # moving the origin branch to a newer commit, but hasn't rebased yet.
5077 upstream_commit = None
5078 cl = Changelist()
5079 upstream_branch = cl.GetUpstreamBranch()
5080 if upstream_branch:
5081 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5082 upstream_commit = upstream_commit.strip()
5083
5084 if not upstream_commit:
5085 DieWithError('Could not find base commit for this branch. '
5086 'Are you in detached state?')
5087
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005088 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5089 diff_output = RunGit(changed_files_cmd)
5090 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005091 # Filter out files deleted by this CL
5092 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005093
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005094 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5095 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5096 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005097 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005098
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005099 top_dir = os.path.normpath(
5100 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5101
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005102 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5103 # formatted. This is used to block during the presubmit.
5104 return_value = 0
5105
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005106 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005107 # Locate the clang-format binary in the checkout
5108 try:
5109 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005110 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005111 DieWithError(e)
5112
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005113 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005114 cmd = [clang_format_tool]
5115 if not opts.dry_run and not opts.diff:
5116 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005117 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005118 if opts.diff:
5119 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005120 else:
5121 env = os.environ.copy()
5122 env['PATH'] = str(os.path.dirname(clang_format_tool))
5123 try:
5124 script = clang_format.FindClangFormatScriptInChromiumTree(
5125 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005126 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005127 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005128
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005129 cmd = [sys.executable, script, '-p0']
5130 if not opts.dry_run and not opts.diff:
5131 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005132
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005133 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5134 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005135
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005136 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5137 if opts.diff:
5138 sys.stdout.write(stdout)
5139 if opts.dry_run and len(stdout) > 0:
5140 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005141
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005142 # Similar code to above, but using yapf on .py files rather than clang-format
5143 # on C/C++ files
5144 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005145 yapf_tool = gclient_utils.FindExecutable('yapf')
5146 if yapf_tool is None:
5147 DieWithError('yapf not found in PATH')
5148
5149 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005150 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005151 cmd = [yapf_tool]
5152 if not opts.dry_run and not opts.diff:
5153 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005154 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005155 if opts.diff:
5156 sys.stdout.write(stdout)
5157 else:
5158 # TODO(sbc): yapf --lines mode still has some issues.
5159 # https://github.com/google/yapf/issues/154
5160 DieWithError('--python currently only works with --full')
5161
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005162 # Dart's formatter does not have the nice property of only operating on
5163 # modified chunks, so hard code full.
5164 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005165 try:
5166 command = [dart_format.FindDartFmtToolInChromiumTree()]
5167 if not opts.dry_run and not opts.diff:
5168 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005169 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005170
ppi@chromium.org6593d932016-03-03 15:41:15 +00005171 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005172 if opts.dry_run and stdout:
5173 return_value = 2
5174 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005175 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5176 'found in this checkout. Files in other languages are still '
5177 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005178
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005179 # Format GN build files. Always run on full build files for canonical form.
5180 if gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005181 cmd = ['gn', 'format' ]
5182 if opts.dry_run or opts.diff:
5183 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005184 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005185 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5186 shell=sys.platform == 'win32',
5187 cwd=top_dir)
5188 if opts.dry_run and gn_ret == 2:
5189 return_value = 2 # Not formatted.
5190 elif opts.diff and gn_ret == 2:
5191 # TODO this should compute and print the actual diff.
5192 print("This change has GN build file diff for " + gn_diff_file)
5193 elif gn_ret != 0:
5194 # For non-dry run cases (and non-2 return values for dry-run), a
5195 # nonzero error code indicates a failure, probably because the file
5196 # doesn't parse.
5197 DieWithError("gn format failed on " + gn_diff_file +
5198 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005199
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005200 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005201
5202
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005203@subcommand.usage('<codereview url or issue id>')
5204def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005205 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005206 _, args = parser.parse_args(args)
5207
5208 if len(args) != 1:
5209 parser.print_help()
5210 return 1
5211
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005212 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005213 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005214 parser.print_help()
5215 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005216 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005217
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005218 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005219 output = RunGit(['config', '--local', '--get-regexp',
5220 r'branch\..*\.%s' % issueprefix],
5221 error_ok=True)
5222 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005223 if issue == target_issue:
5224 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005225
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005226 branches = []
5227 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005228 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005229 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005230 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005231 return 1
5232 if len(branches) == 1:
5233 RunGit(['checkout', branches[0]])
5234 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005235 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005236 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005237 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005238 which = raw_input('Choose by index: ')
5239 try:
5240 RunGit(['checkout', branches[int(which)]])
5241 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005242 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005243 return 1
5244
5245 return 0
5246
5247
maruel@chromium.org29404b52014-09-08 22:58:00 +00005248def CMDlol(parser, args):
5249 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005250 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005251 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5252 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5253 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005254 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005255 return 0
5256
5257
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005258class OptionParser(optparse.OptionParser):
5259 """Creates the option parse and add --verbose support."""
5260 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005261 optparse.OptionParser.__init__(
5262 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005263 self.add_option(
5264 '-v', '--verbose', action='count', default=0,
5265 help='Use 2 times for more debugging info')
5266
5267 def parse_args(self, args=None, values=None):
5268 options, args = optparse.OptionParser.parse_args(self, args, values)
5269 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5270 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5271 return options, args
5272
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005273
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005274def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005275 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005276 print('\nYour python version %s is unsupported, please upgrade.\n' %
5277 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005278 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005279
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005280 # Reload settings.
5281 global settings
5282 settings = Settings()
5283
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005284 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005285 dispatcher = subcommand.CommandDispatcher(__name__)
5286 try:
5287 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005288 except auth.AuthenticationError as e:
5289 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005290 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005291 if e.code != 500:
5292 raise
5293 DieWithError(
5294 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5295 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005296 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005297
5298
5299if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005300 # These affect sys.stdout so do it outside of main() to simplify mocks in
5301 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005302 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005303 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005304 try:
5305 sys.exit(main(sys.argv[1:]))
5306 except KeyboardInterrupt:
5307 sys.stderr.write('interrupted\n')
5308 sys.exit(1)