blob: c6d283c3c56d6c21b234ab0956548be4ce9406a6 [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
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000649
650 def LazyUpdateIfNeeded(self):
651 """Updates the settings from a codereview.settings file, if available."""
652 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000653 # The only value that actually changes the behavior is
654 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000655 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000656 error_ok=True
657 ).strip().lower()
658
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000659 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000660 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000661 LoadCodereviewSettingsFromFile(cr_settings_file)
662 self.updated = True
663
664 def GetDefaultServerUrl(self, error_ok=False):
665 if not self.default_server:
666 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000667 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000668 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000669 if error_ok:
670 return self.default_server
671 if not self.default_server:
672 error_message = ('Could not find settings file. You must configure '
673 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000674 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000675 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000676 return self.default_server
677
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000678 @staticmethod
679 def GetRelativeRoot():
680 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000681
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000682 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000683 if self.root is None:
684 self.root = os.path.abspath(self.GetRelativeRoot())
685 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000686
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000687 def GetGitMirror(self, remote='origin'):
688 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000689 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000690 if not os.path.isdir(local_url):
691 return None
692 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
693 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
694 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
695 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
696 if mirror.exists():
697 return mirror
698 return None
699
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000700 def GetIsGitSvn(self):
701 """Return true if this repo looks like it's using git-svn."""
702 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000703 if self.GetPendingRefPrefix():
704 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
705 self.is_git_svn = False
706 else:
707 # If you have any "svn-remote.*" config keys, we think you're using svn.
708 self.is_git_svn = RunGitWithCode(
709 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000710 return self.is_git_svn
711
712 def GetSVNBranch(self):
713 if self.svn_branch is None:
714 if not self.GetIsGitSvn():
715 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
716
717 # Try to figure out which remote branch we're based on.
718 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000719 # 1) iterate through our branch history and find the svn URL.
720 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000721
722 # regexp matching the git-svn line that contains the URL.
723 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
724
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000725 # We don't want to go through all of history, so read a line from the
726 # pipe at a time.
727 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000728 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000729 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
730 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000731 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000732 for line in proc.stdout:
733 match = git_svn_re.match(line)
734 if match:
735 url = match.group(1)
736 proc.stdout.close() # Cut pipe.
737 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000738
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000739 if url:
740 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
741 remotes = RunGit(['config', '--get-regexp',
742 r'^svn-remote\..*\.url']).splitlines()
743 for remote in remotes:
744 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000745 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000746 remote = match.group(1)
747 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000748 rewrite_root = RunGit(
749 ['config', 'svn-remote.%s.rewriteRoot' % remote],
750 error_ok=True).strip()
751 if rewrite_root:
752 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000753 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000754 ['config', 'svn-remote.%s.fetch' % remote],
755 error_ok=True).strip()
756 if fetch_spec:
757 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
758 if self.svn_branch:
759 break
760 branch_spec = RunGit(
761 ['config', 'svn-remote.%s.branches' % remote],
762 error_ok=True).strip()
763 if branch_spec:
764 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
765 if self.svn_branch:
766 break
767 tag_spec = RunGit(
768 ['config', 'svn-remote.%s.tags' % remote],
769 error_ok=True).strip()
770 if tag_spec:
771 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
772 if self.svn_branch:
773 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000774
775 if not self.svn_branch:
776 DieWithError('Can\'t guess svn branch -- try specifying it on the '
777 'command line')
778
779 return self.svn_branch
780
781 def GetTreeStatusUrl(self, error_ok=False):
782 if not self.tree_status_url:
783 error_message = ('You must configure your tree status URL by running '
784 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000785 self.tree_status_url = self._GetRietveldConfig(
786 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000787 return self.tree_status_url
788
789 def GetViewVCUrl(self):
790 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000791 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000792 return self.viewvc_url
793
rmistry@google.com90752582014-01-14 21:04:50 +0000794 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000795 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000796
rmistry@google.com78948ed2015-07-08 23:09:57 +0000797 def GetIsSkipDependencyUpload(self, branch_name):
798 """Returns true if specified branch should skip dep uploads."""
799 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
800 error_ok=True)
801
rmistry@google.com5626a922015-02-26 14:03:30 +0000802 def GetRunPostUploadHook(self):
803 run_post_upload_hook = self._GetRietveldConfig(
804 'run-post-upload-hook', error_ok=True)
805 return run_post_upload_hook == "True"
806
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000807 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000808 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000809
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000810 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000811 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000812
ukai@chromium.orge8077812012-02-03 03:41:46 +0000813 def GetIsGerrit(self):
814 """Return true if this repo is assosiated with gerrit code review system."""
815 if self.is_gerrit is None:
816 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
817 return self.is_gerrit
818
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000819 def GetSquashGerritUploads(self):
820 """Return true if uploads to Gerrit should be squashed by default."""
821 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700822 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
823 if self.squash_gerrit_uploads is None:
824 # Default is squash now (http://crbug.com/611892#c23).
825 self.squash_gerrit_uploads = not (
826 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
827 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000828 return self.squash_gerrit_uploads
829
tandriia60502f2016-06-20 02:01:53 -0700830 def GetSquashGerritUploadsOverride(self):
831 """Return True or False if codereview.settings should be overridden.
832
833 Returns None if no override has been defined.
834 """
835 # See also http://crbug.com/611892#c23
836 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
837 error_ok=True).strip()
838 if result == 'true':
839 return True
840 if result == 'false':
841 return False
842 return None
843
tandrii@chromium.org28253532016-04-14 13:46:56 +0000844 def GetGerritSkipEnsureAuthenticated(self):
845 """Return True if EnsureAuthenticated should not be done for Gerrit
846 uploads."""
847 if self.gerrit_skip_ensure_authenticated is None:
848 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000849 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000850 error_ok=True).strip() == 'true')
851 return self.gerrit_skip_ensure_authenticated
852
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000853 def GetGitEditor(self):
854 """Return the editor specified in the git config, or None if none is."""
855 if self.git_editor is None:
856 self.git_editor = self._GetConfig('core.editor', error_ok=True)
857 return self.git_editor or None
858
thestig@chromium.org44202a22014-03-11 19:22:18 +0000859 def GetLintRegex(self):
860 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
861 DEFAULT_LINT_REGEX)
862
863 def GetLintIgnoreRegex(self):
864 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
865 DEFAULT_LINT_IGNORE_REGEX)
866
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000867 def GetProject(self):
868 if not self.project:
869 self.project = self._GetRietveldConfig('project', error_ok=True)
870 return self.project
871
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000872 def GetForceHttpsCommitUrl(self):
873 if not self.force_https_commit_url:
874 self.force_https_commit_url = self._GetRietveldConfig(
875 'force-https-commit-url', error_ok=True)
876 return self.force_https_commit_url
877
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000878 def GetPendingRefPrefix(self):
879 if not self.pending_ref_prefix:
880 self.pending_ref_prefix = self._GetRietveldConfig(
881 'pending-ref-prefix', error_ok=True)
882 return self.pending_ref_prefix
883
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000884 def _GetRietveldConfig(self, param, **kwargs):
885 return self._GetConfig('rietveld.' + param, **kwargs)
886
rmistry@google.com78948ed2015-07-08 23:09:57 +0000887 def _GetBranchConfig(self, branch_name, param, **kwargs):
888 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
889
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000890 def _GetConfig(self, param, **kwargs):
891 self.LazyUpdateIfNeeded()
892 return RunGit(['config', param], **kwargs).strip()
893
894
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000895def ShortBranchName(branch):
896 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000897 return branch.replace('refs/heads/', '', 1)
898
899
900def GetCurrentBranchRef():
901 """Returns branch ref (e.g., refs/heads/master) or None."""
902 return RunGit(['symbolic-ref', 'HEAD'],
903 stderr=subprocess2.VOID, error_ok=True).strip() or None
904
905
906def GetCurrentBranch():
907 """Returns current branch or None.
908
909 For refs/heads/* branches, returns just last part. For others, full ref.
910 """
911 branchref = GetCurrentBranchRef()
912 if branchref:
913 return ShortBranchName(branchref)
914 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000915
916
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000917class _CQState(object):
918 """Enum for states of CL with respect to Commit Queue."""
919 NONE = 'none'
920 DRY_RUN = 'dry_run'
921 COMMIT = 'commit'
922
923 ALL_STATES = [NONE, DRY_RUN, COMMIT]
924
925
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000926class _ParsedIssueNumberArgument(object):
927 def __init__(self, issue=None, patchset=None, hostname=None):
928 self.issue = issue
929 self.patchset = patchset
930 self.hostname = hostname
931
932 @property
933 def valid(self):
934 return self.issue is not None
935
936
937class _RietveldParsedIssueNumberArgument(_ParsedIssueNumberArgument):
938 def __init__(self, *args, **kwargs):
939 self.patch_url = kwargs.pop('patch_url', None)
940 super(_RietveldParsedIssueNumberArgument, self).__init__(*args, **kwargs)
941
942
943def ParseIssueNumberArgument(arg):
944 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
945 fail_result = _ParsedIssueNumberArgument()
946
947 if arg.isdigit():
948 return _ParsedIssueNumberArgument(issue=int(arg))
949 if not arg.startswith('http'):
950 return fail_result
951 url = gclient_utils.UpgradeToHttps(arg)
952 try:
953 parsed_url = urlparse.urlparse(url)
954 except ValueError:
955 return fail_result
956 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
957 tmp = cls.ParseIssueURL(parsed_url)
958 if tmp is not None:
959 return tmp
960 return fail_result
961
962
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000963class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000964 """Changelist works with one changelist in local branch.
965
966 Supports two codereview backends: Rietveld or Gerrit, selected at object
967 creation.
968
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +0000969 Notes:
970 * Not safe for concurrent multi-{thread,process} use.
971 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -0700972 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000973 """
974
975 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
976 """Create a new ChangeList instance.
977
978 If issue is given, the codereview must be given too.
979
980 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
981 Otherwise, it's decided based on current configuration of the local branch,
982 with default being 'rietveld' for backwards compatibility.
983 See _load_codereview_impl for more details.
984
985 **kwargs will be passed directly to codereview implementation.
986 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000987 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000988 global settings
989 if not settings:
990 # Happens when git_cl.py is used as a utility library.
991 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000992
993 if issue:
994 assert codereview, 'codereview must be known, if issue is known'
995
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000996 self.branchref = branchref
997 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000998 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000999 self.branch = ShortBranchName(self.branchref)
1000 else:
1001 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001002 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001003 self.lookedup_issue = False
1004 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001005 self.has_description = False
1006 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001007 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001008 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001009 self.cc = None
1010 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001011 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001012
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001013 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001014 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001015 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001016 assert self._codereview_impl
1017 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001018
1019 def _load_codereview_impl(self, codereview=None, **kwargs):
1020 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001021 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1022 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1023 self._codereview = codereview
1024 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001025 return
1026
1027 # Automatic selection based on issue number set for a current branch.
1028 # Rietveld takes precedence over Gerrit.
1029 assert not self.issue
1030 # Whether we find issue or not, we are doing the lookup.
1031 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001032 if self.GetBranch():
1033 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1034 issue = _git_get_branch_config_value(
1035 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1036 if issue:
1037 self._codereview = codereview
1038 self._codereview_impl = cls(self, **kwargs)
1039 self.issue = int(issue)
1040 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001041
1042 # No issue is set for this branch, so decide based on repo-wide settings.
1043 return self._load_codereview_impl(
1044 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1045 **kwargs)
1046
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001047 def IsGerrit(self):
1048 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001049
1050 def GetCCList(self):
1051 """Return the users cc'd on this CL.
1052
agable92bec4f2016-08-24 09:27:27 -07001053 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001054 """
1055 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001056 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001057 more_cc = ','.join(self.watchers)
1058 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1059 return self.cc
1060
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001061 def GetCCListWithoutDefault(self):
1062 """Return the users cc'd on this CL excluding default ones."""
1063 if self.cc is None:
1064 self.cc = ','.join(self.watchers)
1065 return self.cc
1066
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001067 def SetWatchers(self, watchers):
1068 """Set the list of email addresses that should be cc'd based on the changed
1069 files in this CL.
1070 """
1071 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001072
1073 def GetBranch(self):
1074 """Returns the short branch name, e.g. 'master'."""
1075 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001076 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001077 if not branchref:
1078 return None
1079 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001080 self.branch = ShortBranchName(self.branchref)
1081 return self.branch
1082
1083 def GetBranchRef(self):
1084 """Returns the full branch name, e.g. 'refs/heads/master'."""
1085 self.GetBranch() # Poke the lazy loader.
1086 return self.branchref
1087
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001088 def ClearBranch(self):
1089 """Clears cached branch data of this object."""
1090 self.branch = self.branchref = None
1091
tandrii5d48c322016-08-18 16:19:37 -07001092 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1093 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1094 kwargs['branch'] = self.GetBranch()
1095 return _git_get_branch_config_value(key, default, **kwargs)
1096
1097 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1098 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1099 assert self.GetBranch(), (
1100 'this CL must have an associated branch to %sset %s%s' %
1101 ('un' if value is None else '',
1102 key,
1103 '' if value is None else ' to %r' % value))
1104 kwargs['branch'] = self.GetBranch()
1105 return _git_set_branch_config_value(key, value, **kwargs)
1106
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001107 @staticmethod
1108 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001109 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001110 e.g. 'origin', 'refs/heads/master'
1111 """
1112 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001113 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1114
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001115 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001116 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001117 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001118 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1119 error_ok=True).strip()
1120 if upstream_branch:
1121 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001122 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001123 # Fall back on trying a git-svn upstream branch.
1124 if settings.GetIsGitSvn():
1125 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001126 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001127 # Else, try to guess the origin remote.
1128 remote_branches = RunGit(['branch', '-r']).split()
1129 if 'origin/master' in remote_branches:
1130 # Fall back on origin/master if it exits.
1131 remote = 'origin'
1132 upstream_branch = 'refs/heads/master'
1133 elif 'origin/trunk' in remote_branches:
1134 # Fall back on origin/trunk if it exists. Generally a shared
1135 # git-svn clone
1136 remote = 'origin'
1137 upstream_branch = 'refs/heads/trunk'
1138 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001139 DieWithError(
1140 'Unable to determine default branch to diff against.\n'
1141 'Either pass complete "git diff"-style arguments, like\n'
1142 ' git cl upload origin/master\n'
1143 'or verify this branch is set up to track another \n'
1144 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001145
1146 return remote, upstream_branch
1147
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001148 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001149 upstream_branch = self.GetUpstreamBranch()
1150 if not BranchExists(upstream_branch):
1151 DieWithError('The upstream for the current branch (%s) does not exist '
1152 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001153 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001154 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001155
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001156 def GetUpstreamBranch(self):
1157 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001158 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001159 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001160 upstream_branch = upstream_branch.replace('refs/heads/',
1161 'refs/remotes/%s/' % remote)
1162 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1163 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001164 self.upstream_branch = upstream_branch
1165 return self.upstream_branch
1166
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001167 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001168 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001169 remote, branch = None, self.GetBranch()
1170 seen_branches = set()
1171 while branch not in seen_branches:
1172 seen_branches.add(branch)
1173 remote, branch = self.FetchUpstreamTuple(branch)
1174 branch = ShortBranchName(branch)
1175 if remote != '.' or branch.startswith('refs/remotes'):
1176 break
1177 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001178 remotes = RunGit(['remote'], error_ok=True).split()
1179 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001180 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001181 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001182 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001183 logging.warning('Could not determine which remote this change is '
1184 'associated with, so defaulting to "%s". This may '
1185 'not be what you want. You may prevent this message '
1186 'by running "git svn info" as documented here: %s',
1187 self._remote,
1188 GIT_INSTRUCTIONS_URL)
1189 else:
1190 logging.warn('Could not determine which remote this change is '
1191 'associated with. You may prevent this message by '
1192 'running "git svn info" as documented here: %s',
1193 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001194 branch = 'HEAD'
1195 if branch.startswith('refs/remotes'):
1196 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001197 elif branch.startswith('refs/branch-heads/'):
1198 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001199 else:
1200 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001201 return self._remote
1202
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001203 def GitSanityChecks(self, upstream_git_obj):
1204 """Checks git repo status and ensures diff is from local commits."""
1205
sbc@chromium.org79706062015-01-14 21:18:12 +00001206 if upstream_git_obj is None:
1207 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001208 print('ERROR: unable to determine current branch (detached HEAD?)',
1209 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001210 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001211 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001212 return False
1213
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001214 # Verify the commit we're diffing against is in our current branch.
1215 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1216 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1217 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001218 print('ERROR: %s is not in the current branch. You may need to rebase '
1219 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001220 return False
1221
1222 # List the commits inside the diff, and verify they are all local.
1223 commits_in_diff = RunGit(
1224 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1225 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1226 remote_branch = remote_branch.strip()
1227 if code != 0:
1228 _, remote_branch = self.GetRemoteBranch()
1229
1230 commits_in_remote = RunGit(
1231 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1232
1233 common_commits = set(commits_in_diff) & set(commits_in_remote)
1234 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001235 print('ERROR: Your diff contains %d commits already in %s.\n'
1236 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1237 'the diff. If you are using a custom git flow, you can override'
1238 ' the reference used for this check with "git config '
1239 'gitcl.remotebranch <git-ref>".' % (
1240 len(common_commits), remote_branch, upstream_git_obj),
1241 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001242 return False
1243 return True
1244
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001245 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001246 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001247
1248 Returns None if it is not set.
1249 """
tandrii5d48c322016-08-18 16:19:37 -07001250 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001251
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001252 def GetGitSvnRemoteUrl(self):
1253 """Return the configured git-svn remote URL parsed from git svn info.
1254
1255 Returns None if it is not set.
1256 """
1257 # URL is dependent on the current directory.
1258 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1259 if data:
1260 keys = dict(line.split(': ', 1) for line in data.splitlines()
1261 if ': ' in line)
1262 return keys.get('URL', None)
1263 return None
1264
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001265 def GetRemoteUrl(self):
1266 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1267
1268 Returns None if there is no remote.
1269 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001270 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001271 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1272
1273 # If URL is pointing to a local directory, it is probably a git cache.
1274 if os.path.isdir(url):
1275 url = RunGit(['config', 'remote.%s.url' % remote],
1276 error_ok=True,
1277 cwd=url).strip()
1278 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001279
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001280 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001281 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001282 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001283 self.issue = self._GitGetBranchConfigValue(
1284 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001285 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001286 return self.issue
1287
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001288 def GetIssueURL(self):
1289 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001290 issue = self.GetIssue()
1291 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001292 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001293 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001294
1295 def GetDescription(self, pretty=False):
1296 if not self.has_description:
1297 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001298 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001299 self.has_description = True
1300 if pretty:
1301 wrapper = textwrap.TextWrapper()
1302 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1303 return wrapper.fill(self.description)
1304 return self.description
1305
1306 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001307 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001308 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001309 self.patchset = self._GitGetBranchConfigValue(
1310 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001311 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001312 return self.patchset
1313
1314 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001315 """Set this branch's patchset. If patchset=0, clears the patchset."""
1316 assert self.GetBranch()
1317 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001318 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001319 else:
1320 self.patchset = int(patchset)
1321 self._GitSetBranchConfigValue(
1322 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001323
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001324 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001325 """Set this branch's issue. If issue isn't given, clears the issue."""
1326 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001327 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001328 issue = int(issue)
1329 self._GitSetBranchConfigValue(
1330 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001331 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001332 codereview_server = self._codereview_impl.GetCodereviewServer()
1333 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001334 self._GitSetBranchConfigValue(
1335 self._codereview_impl.CodereviewServerConfigKey(),
1336 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001337 else:
tandrii5d48c322016-08-18 16:19:37 -07001338 # Reset all of these just to be clean.
1339 reset_suffixes = [
1340 'last-upload-hash',
1341 self._codereview_impl.IssueConfigKey(),
1342 self._codereview_impl.PatchsetConfigKey(),
1343 self._codereview_impl.CodereviewServerConfigKey(),
1344 ] + self._PostUnsetIssueProperties()
1345 for prop in reset_suffixes:
1346 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001347 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001348 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001349
dnjba1b0f32016-09-02 12:37:42 -07001350 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001351 if not self.GitSanityChecks(upstream_branch):
1352 DieWithError('\nGit sanity check failure')
1353
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001354 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001355 if not root:
1356 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001357 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001358
1359 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001360 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001361 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001362 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001363 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001364 except subprocess2.CalledProcessError:
1365 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001366 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001367 'This branch probably doesn\'t exist anymore. To reset the\n'
1368 'tracking branch, please run\n'
1369 ' git branch --set-upstream %s trunk\n'
1370 'replacing trunk with origin/master or the relevant branch') %
1371 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001372
maruel@chromium.org52424302012-08-29 15:14:30 +00001373 issue = self.GetIssue()
1374 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001375 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001376 description = self.GetDescription()
1377 else:
1378 # If the change was never uploaded, use the log messages of all commits
1379 # up to the branch point, as git cl upload will prefill the description
1380 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001381 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1382 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001383
1384 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001385 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001386 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001387 name,
1388 description,
1389 absroot,
1390 files,
1391 issue,
1392 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001393 author,
1394 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001395
dsansomee2d6fd92016-09-08 00:10:47 -07001396 def UpdateDescription(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001397 self.description = description
dsansomee2d6fd92016-09-08 00:10:47 -07001398 return self._codereview_impl.UpdateDescriptionRemote(
1399 description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001400
1401 def RunHook(self, committing, may_prompt, verbose, change):
1402 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1403 try:
1404 return presubmit_support.DoPresubmitChecks(change, committing,
1405 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1406 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001407 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1408 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001409 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001410 DieWithError(
1411 ('%s\nMaybe your depot_tools is out of date?\n'
1412 'If all fails, contact maruel@') % e)
1413
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001414 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1415 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001416 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1417 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001418 else:
1419 # Assume url.
1420 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1421 urlparse.urlparse(issue_arg))
1422 if not parsed_issue_arg or not parsed_issue_arg.valid:
1423 DieWithError('Failed to parse issue argument "%s". '
1424 'Must be an issue number or a valid URL.' % issue_arg)
1425 return self._codereview_impl.CMDPatchWithParsedIssue(
1426 parsed_issue_arg, reject, nocommit, directory)
1427
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001428 def CMDUpload(self, options, git_diff_args, orig_args):
1429 """Uploads a change to codereview."""
1430 if git_diff_args:
1431 # TODO(ukai): is it ok for gerrit case?
1432 base_branch = git_diff_args[0]
1433 else:
1434 if self.GetBranch() is None:
1435 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1436
1437 # Default to diffing against common ancestor of upstream branch
1438 base_branch = self.GetCommonAncestorWithUpstream()
1439 git_diff_args = [base_branch, 'HEAD']
1440
1441 # Make sure authenticated to codereview before running potentially expensive
1442 # hooks. It is a fast, best efforts check. Codereview still can reject the
1443 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001444 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001445
1446 # Apply watchlists on upload.
1447 change = self.GetChange(base_branch, None)
1448 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1449 files = [f.LocalPath() for f in change.AffectedFiles()]
1450 if not options.bypass_watchlists:
1451 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1452
1453 if not options.bypass_hooks:
1454 if options.reviewers or options.tbr_owners:
1455 # Set the reviewer list now so that presubmit checks can access it.
1456 change_description = ChangeDescription(change.FullDescriptionText())
1457 change_description.update_reviewers(options.reviewers,
1458 options.tbr_owners,
1459 change)
1460 change.SetDescriptionText(change_description.description)
1461 hook_results = self.RunHook(committing=False,
1462 may_prompt=not options.force,
1463 verbose=options.verbose,
1464 change=change)
1465 if not hook_results.should_continue():
1466 return 1
1467 if not options.reviewers and hook_results.reviewers:
1468 options.reviewers = hook_results.reviewers.split(',')
1469
1470 if self.GetIssue():
1471 latest_patchset = self.GetMostRecentPatchset()
1472 local_patchset = self.GetPatchset()
1473 if (latest_patchset and local_patchset and
1474 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001475 print('The last upload made from this repository was patchset #%d but '
1476 'the most recent patchset on the server is #%d.'
1477 % (local_patchset, latest_patchset))
1478 print('Uploading will still work, but if you\'ve uploaded to this '
1479 'issue from another machine or branch the patch you\'re '
1480 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001481 ask_for_data('About to upload; enter to confirm.')
1482
1483 print_stats(options.similarity, options.find_copies, git_diff_args)
1484 ret = self.CMDUploadChange(options, git_diff_args, change)
1485 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001486 if options.use_commit_queue:
1487 self.SetCQState(_CQState.COMMIT)
1488 elif options.cq_dry_run:
1489 self.SetCQState(_CQState.DRY_RUN)
1490
tandrii5d48c322016-08-18 16:19:37 -07001491 _git_set_branch_config_value('last-upload-hash',
1492 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001493 # Run post upload hooks, if specified.
1494 if settings.GetRunPostUploadHook():
1495 presubmit_support.DoPostUploadExecuter(
1496 change,
1497 self,
1498 settings.GetRoot(),
1499 options.verbose,
1500 sys.stdout)
1501
1502 # Upload all dependencies if specified.
1503 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001504 print()
1505 print('--dependencies has been specified.')
1506 print('All dependent local branches will be re-uploaded.')
1507 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001508 # Remove the dependencies flag from args so that we do not end up in a
1509 # loop.
1510 orig_args.remove('--dependencies')
1511 ret = upload_branch_deps(self, orig_args)
1512 return ret
1513
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001514 def SetCQState(self, new_state):
1515 """Update the CQ state for latest patchset.
1516
1517 Issue must have been already uploaded and known.
1518 """
1519 assert new_state in _CQState.ALL_STATES
1520 assert self.GetIssue()
1521 return self._codereview_impl.SetCQState(new_state)
1522
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001523 # Forward methods to codereview specific implementation.
1524
1525 def CloseIssue(self):
1526 return self._codereview_impl.CloseIssue()
1527
1528 def GetStatus(self):
1529 return self._codereview_impl.GetStatus()
1530
1531 def GetCodereviewServer(self):
1532 return self._codereview_impl.GetCodereviewServer()
1533
1534 def GetApprovingReviewers(self):
1535 return self._codereview_impl.GetApprovingReviewers()
1536
1537 def GetMostRecentPatchset(self):
1538 return self._codereview_impl.GetMostRecentPatchset()
1539
1540 def __getattr__(self, attr):
1541 # This is because lots of untested code accesses Rietveld-specific stuff
1542 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001543 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001544 # Note that child method defines __getattr__ as well, and forwards it here,
1545 # because _RietveldChangelistImpl is not cleaned up yet, and given
1546 # deprecation of Rietveld, it should probably be just removed.
1547 # Until that time, avoid infinite recursion by bypassing __getattr__
1548 # of implementation class.
1549 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001550
1551
1552class _ChangelistCodereviewBase(object):
1553 """Abstract base class encapsulating codereview specifics of a changelist."""
1554 def __init__(self, changelist):
1555 self._changelist = changelist # instance of Changelist
1556
1557 def __getattr__(self, attr):
1558 # Forward methods to changelist.
1559 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1560 # _RietveldChangelistImpl to avoid this hack?
1561 return getattr(self._changelist, attr)
1562
1563 def GetStatus(self):
1564 """Apply a rough heuristic to give a simple summary of an issue's review
1565 or CQ status, assuming adherence to a common workflow.
1566
1567 Returns None if no issue for this branch, or specific string keywords.
1568 """
1569 raise NotImplementedError()
1570
1571 def GetCodereviewServer(self):
1572 """Returns server URL without end slash, like "https://codereview.com"."""
1573 raise NotImplementedError()
1574
1575 def FetchDescription(self):
1576 """Fetches and returns description from the codereview server."""
1577 raise NotImplementedError()
1578
tandrii5d48c322016-08-18 16:19:37 -07001579 @classmethod
1580 def IssueConfigKey(cls):
1581 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001582 raise NotImplementedError()
1583
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001584 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001585 def PatchsetConfigKey(cls):
1586 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001587 raise NotImplementedError()
1588
tandrii5d48c322016-08-18 16:19:37 -07001589 @classmethod
1590 def CodereviewServerConfigKey(cls):
1591 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001592 raise NotImplementedError()
1593
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001594 def _PostUnsetIssueProperties(self):
1595 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001596 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001597
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001598 def GetRieveldObjForPresubmit(self):
1599 # This is an unfortunate Rietveld-embeddedness in presubmit.
1600 # For non-Rietveld codereviews, this probably should return a dummy object.
1601 raise NotImplementedError()
1602
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001603 def GetGerritObjForPresubmit(self):
1604 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1605 return None
1606
dsansomee2d6fd92016-09-08 00:10:47 -07001607 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001608 """Update the description on codereview site."""
1609 raise NotImplementedError()
1610
1611 def CloseIssue(self):
1612 """Closes the issue."""
1613 raise NotImplementedError()
1614
1615 def GetApprovingReviewers(self):
1616 """Returns a list of reviewers approving the change.
1617
1618 Note: not necessarily committers.
1619 """
1620 raise NotImplementedError()
1621
1622 def GetMostRecentPatchset(self):
1623 """Returns the most recent patchset number from the codereview site."""
1624 raise NotImplementedError()
1625
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001626 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1627 directory):
1628 """Fetches and applies the issue.
1629
1630 Arguments:
1631 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1632 reject: if True, reject the failed patch instead of switching to 3-way
1633 merge. Rietveld only.
1634 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1635 only.
1636 directory: switch to directory before applying the patch. Rietveld only.
1637 """
1638 raise NotImplementedError()
1639
1640 @staticmethod
1641 def ParseIssueURL(parsed_url):
1642 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1643 failed."""
1644 raise NotImplementedError()
1645
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001646 def EnsureAuthenticated(self, force):
1647 """Best effort check that user is authenticated with codereview server.
1648
1649 Arguments:
1650 force: whether to skip confirmation questions.
1651 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001652 raise NotImplementedError()
1653
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001654 def CMDUploadChange(self, options, args, change):
1655 """Uploads a change to codereview."""
1656 raise NotImplementedError()
1657
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001658 def SetCQState(self, new_state):
1659 """Update the CQ state for latest patchset.
1660
1661 Issue must have been already uploaded and known.
1662 """
1663 raise NotImplementedError()
1664
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001665
1666class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1667 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1668 super(_RietveldChangelistImpl, self).__init__(changelist)
1669 assert settings, 'must be initialized in _ChangelistCodereviewBase'
martiniss6eda05f2016-06-30 10:18:35 -07001670 if not rietveld_server:
1671 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001672
1673 self._rietveld_server = rietveld_server
1674 self._auth_config = auth_config
1675 self._props = None
1676 self._rpc_server = None
1677
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001678 def GetCodereviewServer(self):
1679 if not self._rietveld_server:
1680 # If we're on a branch then get the server potentially associated
1681 # with that branch.
1682 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001683 self._rietveld_server = gclient_utils.UpgradeToHttps(
1684 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001685 if not self._rietveld_server:
1686 self._rietveld_server = settings.GetDefaultServerUrl()
1687 return self._rietveld_server
1688
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001689 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001690 """Best effort check that user is authenticated with Rietveld server."""
1691 if self._auth_config.use_oauth2:
1692 authenticator = auth.get_authenticator_for_host(
1693 self.GetCodereviewServer(), self._auth_config)
1694 if not authenticator.has_cached_credentials():
1695 raise auth.LoginRequiredError(self.GetCodereviewServer())
1696
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001697 def FetchDescription(self):
1698 issue = self.GetIssue()
1699 assert issue
1700 try:
1701 return self.RpcServer().get_description(issue).strip()
1702 except urllib2.HTTPError as e:
1703 if e.code == 404:
1704 DieWithError(
1705 ('\nWhile fetching the description for issue %d, received a '
1706 '404 (not found)\n'
1707 'error. It is likely that you deleted this '
1708 'issue on the server. If this is the\n'
1709 'case, please run\n\n'
1710 ' git cl issue 0\n\n'
1711 'to clear the association with the deleted issue. Then run '
1712 'this command again.') % issue)
1713 else:
1714 DieWithError(
1715 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1716 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001717 print('Warning: Failed to retrieve CL description due to network '
1718 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001719 return ''
1720
1721 def GetMostRecentPatchset(self):
1722 return self.GetIssueProperties()['patchsets'][-1]
1723
1724 def GetPatchSetDiff(self, issue, patchset):
1725 return self.RpcServer().get(
1726 '/download/issue%s_%s.diff' % (issue, patchset))
1727
1728 def GetIssueProperties(self):
1729 if self._props is None:
1730 issue = self.GetIssue()
1731 if not issue:
1732 self._props = {}
1733 else:
1734 self._props = self.RpcServer().get_issue_properties(issue, True)
1735 return self._props
1736
1737 def GetApprovingReviewers(self):
1738 return get_approving_reviewers(self.GetIssueProperties())
1739
1740 def AddComment(self, message):
1741 return self.RpcServer().add_comment(self.GetIssue(), message)
1742
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001743 def GetStatus(self):
1744 """Apply a rough heuristic to give a simple summary of an issue's review
1745 or CQ status, assuming adherence to a common workflow.
1746
1747 Returns None if no issue for this branch, or one of the following keywords:
1748 * 'error' - error from review tool (including deleted issues)
1749 * 'unsent' - not sent for review
1750 * 'waiting' - waiting for review
1751 * 'reply' - waiting for owner to reply to review
1752 * 'lgtm' - LGTM from at least one approved reviewer
1753 * 'commit' - in the commit queue
1754 * 'closed' - closed
1755 """
1756 if not self.GetIssue():
1757 return None
1758
1759 try:
1760 props = self.GetIssueProperties()
1761 except urllib2.HTTPError:
1762 return 'error'
1763
1764 if props.get('closed'):
1765 # Issue is closed.
1766 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001767 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001768 # Issue is in the commit queue.
1769 return 'commit'
1770
1771 try:
1772 reviewers = self.GetApprovingReviewers()
1773 except urllib2.HTTPError:
1774 return 'error'
1775
1776 if reviewers:
1777 # Was LGTM'ed.
1778 return 'lgtm'
1779
1780 messages = props.get('messages') or []
1781
tandrii9d2c7a32016-06-22 03:42:45 -07001782 # Skip CQ messages that don't require owner's action.
1783 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1784 if 'Dry run:' in messages[-1]['text']:
1785 messages.pop()
1786 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1787 # This message always follows prior messages from CQ,
1788 # so skip this too.
1789 messages.pop()
1790 else:
1791 # This is probably a CQ messages warranting user attention.
1792 break
1793
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001794 if not messages:
1795 # No message was sent.
1796 return 'unsent'
1797 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07001798 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001799 return 'reply'
1800 return 'waiting'
1801
dsansomee2d6fd92016-09-08 00:10:47 -07001802 def UpdateDescriptionRemote(self, description, force=False):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001803 return self.RpcServer().update_description(
1804 self.GetIssue(), self.description)
1805
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001806 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001807 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001808
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001809 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07001810 return self.SetFlags({flag: value})
1811
1812 def SetFlags(self, flags):
1813 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07001814 """
phajdan.jr68598232016-08-10 03:28:28 -07001815 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001816 try:
tandrii4b233bd2016-07-06 03:50:29 -07001817 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07001818 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07001819 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001820 if e.code == 404:
1821 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1822 if e.code == 403:
1823 DieWithError(
1824 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07001825 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001826 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001827
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001828 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001829 """Returns an upload.RpcServer() to access this review's rietveld instance.
1830 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001831 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001832 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001833 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001834 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001835 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001836
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001837 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001838 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001839 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001840
tandrii5d48c322016-08-18 16:19:37 -07001841 @classmethod
1842 def PatchsetConfigKey(cls):
1843 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001844
tandrii5d48c322016-08-18 16:19:37 -07001845 @classmethod
1846 def CodereviewServerConfigKey(cls):
1847 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001848
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001849 def GetRieveldObjForPresubmit(self):
1850 return self.RpcServer()
1851
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001852 def SetCQState(self, new_state):
1853 props = self.GetIssueProperties()
1854 if props.get('private'):
1855 DieWithError('Cannot set-commit on private issue')
1856
1857 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07001858 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001859 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07001860 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001861 else:
tandrii4b233bd2016-07-06 03:50:29 -07001862 assert new_state == _CQState.DRY_RUN
1863 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001864
1865
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001866 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1867 directory):
1868 # TODO(maruel): Use apply_issue.py
1869
1870 # PatchIssue should never be called with a dirty tree. It is up to the
1871 # caller to check this, but just in case we assert here since the
1872 # consequences of the caller not checking this could be dire.
1873 assert(not git_common.is_dirty_git_tree('apply'))
1874 assert(parsed_issue_arg.valid)
1875 self._changelist.issue = parsed_issue_arg.issue
1876 if parsed_issue_arg.hostname:
1877 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1878
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001879 if (isinstance(parsed_issue_arg, _RietveldParsedIssueNumberArgument) and
1880 parsed_issue_arg.patch_url):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001881 assert parsed_issue_arg.patchset
1882 patchset = parsed_issue_arg.patchset
1883 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1884 else:
1885 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1886 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1887
1888 # Switch up to the top-level directory, if necessary, in preparation for
1889 # applying the patch.
1890 top = settings.GetRelativeRoot()
1891 if top:
1892 os.chdir(top)
1893
1894 # Git patches have a/ at the beginning of source paths. We strip that out
1895 # with a sed script rather than the -p flag to patch so we can feed either
1896 # Git or svn-style patches into the same apply command.
1897 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1898 try:
1899 patch_data = subprocess2.check_output(
1900 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1901 except subprocess2.CalledProcessError:
1902 DieWithError('Git patch mungling failed.')
1903 logging.info(patch_data)
1904
1905 # We use "git apply" to apply the patch instead of "patch" so that we can
1906 # pick up file adds.
1907 # The --index flag means: also insert into the index (so we catch adds).
1908 cmd = ['git', 'apply', '--index', '-p0']
1909 if directory:
1910 cmd.extend(('--directory', directory))
1911 if reject:
1912 cmd.append('--reject')
1913 elif IsGitVersionAtLeast('1.7.12'):
1914 cmd.append('--3way')
1915 try:
1916 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
1917 stdin=patch_data, stdout=subprocess2.VOID)
1918 except subprocess2.CalledProcessError:
vapiera7fbd5a2016-06-16 09:17:49 -07001919 print('Failed to apply the patch')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001920 return 1
1921
1922 # If we had an issue, commit the current state and register the issue.
1923 if not nocommit:
1924 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
1925 'patch from issue %(i)s at patchset '
1926 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
1927 % {'i': self.GetIssue(), 'p': patchset})])
1928 self.SetIssue(self.GetIssue())
1929 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07001930 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001931 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001932 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001933 return 0
1934
1935 @staticmethod
1936 def ParseIssueURL(parsed_url):
1937 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1938 return None
wychen3c1c1722016-08-04 11:46:36 -07001939 # Rietveld patch: https://domain/<number>/#ps<patchset>
1940 match = re.match(r'/(\d+)/$', parsed_url.path)
1941 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
1942 if match and match2:
1943 return _RietveldParsedIssueNumberArgument(
1944 issue=int(match.group(1)),
1945 patchset=int(match2.group(1)),
1946 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001947 # Typical url: https://domain/<issue_number>[/[other]]
1948 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
1949 if match:
1950 return _RietveldParsedIssueNumberArgument(
1951 issue=int(match.group(1)),
1952 hostname=parsed_url.netloc)
1953 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
1954 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
1955 if match:
1956 return _RietveldParsedIssueNumberArgument(
1957 issue=int(match.group(1)),
1958 patchset=int(match.group(2)),
1959 hostname=parsed_url.netloc,
1960 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
1961 return None
1962
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001963 def CMDUploadChange(self, options, args, change):
1964 """Upload the patch to Rietveld."""
1965 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1966 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001967 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
1968 if options.emulate_svn_auto_props:
1969 upload_args.append('--emulate_svn_auto_props')
1970
1971 change_desc = None
1972
1973 if options.email is not None:
1974 upload_args.extend(['--email', options.email])
1975
1976 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07001977 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001978 upload_args.extend(['--title', options.title])
1979 if options.message:
1980 upload_args.extend(['--message', options.message])
1981 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07001982 print('This branch is associated with issue %s. '
1983 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001984 else:
nodirca166002016-06-27 10:59:51 -07001985 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001986 upload_args.extend(['--title', options.title])
1987 message = (options.title or options.message or
1988 CreateDescriptionFromLog(args))
1989 change_desc = ChangeDescription(message)
1990 if options.reviewers or options.tbr_owners:
1991 change_desc.update_reviewers(options.reviewers,
1992 options.tbr_owners,
1993 change)
1994 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07001995 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001996
1997 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07001998 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001999 return 1
2000
2001 upload_args.extend(['--message', change_desc.description])
2002 if change_desc.get_reviewers():
2003 upload_args.append('--reviewers=%s' % ','.join(
2004 change_desc.get_reviewers()))
2005 if options.send_mail:
2006 if not change_desc.get_reviewers():
2007 DieWithError("Must specify reviewers to send email.")
2008 upload_args.append('--send_mail')
2009
2010 # We check this before applying rietveld.private assuming that in
2011 # rietveld.cc only addresses which we can send private CLs to are listed
2012 # if rietveld.private is set, and so we should ignore rietveld.cc only
2013 # when --private is specified explicitly on the command line.
2014 if options.private:
2015 logging.warn('rietveld.cc is ignored since private flag is specified. '
2016 'You need to review and add them manually if necessary.')
2017 cc = self.GetCCListWithoutDefault()
2018 else:
2019 cc = self.GetCCList()
2020 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
2021 if cc:
2022 upload_args.extend(['--cc', cc])
2023
2024 if options.private or settings.GetDefaultPrivateFlag() == "True":
2025 upload_args.append('--private')
2026
2027 upload_args.extend(['--git_similarity', str(options.similarity)])
2028 if not options.find_copies:
2029 upload_args.extend(['--git_no_find_copies'])
2030
2031 # Include the upstream repo's URL in the change -- this is useful for
2032 # projects that have their source spread across multiple repos.
2033 remote_url = self.GetGitBaseUrlFromConfig()
2034 if not remote_url:
2035 if settings.GetIsGitSvn():
2036 remote_url = self.GetGitSvnRemoteUrl()
2037 else:
2038 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2039 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2040 self.GetUpstreamBranch().split('/')[-1])
2041 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002042 remote, remote_branch = self.GetRemoteBranch()
2043 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2044 settings.GetPendingRefPrefix())
2045 if target_ref:
2046 upload_args.extend(['--target_ref', target_ref])
2047
2048 # Look for dependent patchsets. See crbug.com/480453 for more details.
2049 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2050 upstream_branch = ShortBranchName(upstream_branch)
2051 if remote is '.':
2052 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002053 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002054 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002055 print()
2056 print('Skipping dependency patchset upload because git config '
2057 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2058 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002059 else:
2060 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002061 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002062 auth_config=auth_config)
2063 branch_cl_issue_url = branch_cl.GetIssueURL()
2064 branch_cl_issue = branch_cl.GetIssue()
2065 branch_cl_patchset = branch_cl.GetPatchset()
2066 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2067 upload_args.extend(
2068 ['--depends_on_patchset', '%s:%s' % (
2069 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002070 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002071 '\n'
2072 'The current branch (%s) is tracking a local branch (%s) with '
2073 'an associated CL.\n'
2074 'Adding %s/#ps%s as a dependency patchset.\n'
2075 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2076 branch_cl_patchset))
2077
2078 project = settings.GetProject()
2079 if project:
2080 upload_args.extend(['--project', project])
2081
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002082 try:
2083 upload_args = ['upload'] + upload_args + args
2084 logging.info('upload.RealMain(%s)', upload_args)
2085 issue, patchset = upload.RealMain(upload_args)
2086 issue = int(issue)
2087 patchset = int(patchset)
2088 except KeyboardInterrupt:
2089 sys.exit(1)
2090 except:
2091 # If we got an exception after the user typed a description for their
2092 # change, back up the description before re-raising.
2093 if change_desc:
2094 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2095 print('\nGot exception while uploading -- saving description to %s\n' %
2096 backup_path)
2097 backup_file = open(backup_path, 'w')
2098 backup_file.write(change_desc.description)
2099 backup_file.close()
2100 raise
2101
2102 if not self.GetIssue():
2103 self.SetIssue(issue)
2104 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002105 return 0
2106
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002107
2108class _GerritChangelistImpl(_ChangelistCodereviewBase):
2109 def __init__(self, changelist, auth_config=None):
2110 # auth_config is Rietveld thing, kept here to preserve interface only.
2111 super(_GerritChangelistImpl, self).__init__(changelist)
2112 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002113 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002114 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002115 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002116
2117 def _GetGerritHost(self):
2118 # Lazy load of configs.
2119 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002120 if self._gerrit_host and '.' not in self._gerrit_host:
2121 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2122 # This happens for internal stuff http://crbug.com/614312.
2123 parsed = urlparse.urlparse(self.GetRemoteUrl())
2124 if parsed.scheme == 'sso':
2125 print('WARNING: using non https URLs for remote is likely broken\n'
2126 ' Your current remote is: %s' % self.GetRemoteUrl())
2127 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2128 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002129 return self._gerrit_host
2130
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002131 def _GetGitHost(self):
2132 """Returns git host to be used when uploading change to Gerrit."""
2133 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2134
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002135 def GetCodereviewServer(self):
2136 if not self._gerrit_server:
2137 # If we're on a branch then get the server potentially associated
2138 # with that branch.
2139 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002140 self._gerrit_server = self._GitGetBranchConfigValue(
2141 self.CodereviewServerConfigKey())
2142 if self._gerrit_server:
2143 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002144 if not self._gerrit_server:
2145 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2146 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002147 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002148 parts[0] = parts[0] + '-review'
2149 self._gerrit_host = '.'.join(parts)
2150 self._gerrit_server = 'https://%s' % self._gerrit_host
2151 return self._gerrit_server
2152
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002153 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002154 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002155 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002156
tandrii5d48c322016-08-18 16:19:37 -07002157 @classmethod
2158 def PatchsetConfigKey(cls):
2159 return 'gerritpatchset'
2160
2161 @classmethod
2162 def CodereviewServerConfigKey(cls):
2163 return 'gerritserver'
2164
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002165 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002166 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002167 if settings.GetGerritSkipEnsureAuthenticated():
2168 # For projects with unusual authentication schemes.
2169 # See http://crbug.com/603378.
2170 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002171 # Lazy-loader to identify Gerrit and Git hosts.
2172 if gerrit_util.GceAuthenticator.is_gce():
2173 return
2174 self.GetCodereviewServer()
2175 git_host = self._GetGitHost()
2176 assert self._gerrit_server and self._gerrit_host
2177 cookie_auth = gerrit_util.CookiesAuthenticator()
2178
2179 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2180 git_auth = cookie_auth.get_auth_header(git_host)
2181 if gerrit_auth and git_auth:
2182 if gerrit_auth == git_auth:
2183 return
2184 print((
2185 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2186 ' Check your %s or %s file for credentials of hosts:\n'
2187 ' %s\n'
2188 ' %s\n'
2189 ' %s') %
2190 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2191 git_host, self._gerrit_host,
2192 cookie_auth.get_new_password_message(git_host)))
2193 if not force:
2194 ask_for_data('If you know what you are doing, press Enter to continue, '
2195 'Ctrl+C to abort.')
2196 return
2197 else:
2198 missing = (
2199 [] if gerrit_auth else [self._gerrit_host] +
2200 [] if git_auth else [git_host])
2201 DieWithError('Credentials for the following hosts are required:\n'
2202 ' %s\n'
2203 'These are read from %s (or legacy %s)\n'
2204 '%s' % (
2205 '\n '.join(missing),
2206 cookie_auth.get_gitcookies_path(),
2207 cookie_auth.get_netrc_path(),
2208 cookie_auth.get_new_password_message(git_host)))
2209
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002210 def _PostUnsetIssueProperties(self):
2211 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002212 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002213
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002214 def GetRieveldObjForPresubmit(self):
2215 class ThisIsNotRietveldIssue(object):
2216 def __nonzero__(self):
2217 # This is a hack to make presubmit_support think that rietveld is not
2218 # defined, yet still ensure that calls directly result in a decent
2219 # exception message below.
2220 return False
2221
2222 def __getattr__(self, attr):
2223 print(
2224 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2225 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2226 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2227 'or use Rietveld for codereview.\n'
2228 'See also http://crbug.com/579160.' % attr)
2229 raise NotImplementedError()
2230 return ThisIsNotRietveldIssue()
2231
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002232 def GetGerritObjForPresubmit(self):
2233 return presubmit_support.GerritAccessor(self._GetGerritHost())
2234
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002235 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002236 """Apply a rough heuristic to give a simple summary of an issue's review
2237 or CQ status, assuming adherence to a common workflow.
2238
2239 Returns None if no issue for this branch, or one of the following keywords:
2240 * 'error' - error from review tool (including deleted issues)
2241 * 'unsent' - no reviewers added
2242 * 'waiting' - waiting for review
2243 * 'reply' - waiting for owner to reply to review
2244 * 'not lgtm' - Code-Review -2 from at least one approved reviewer
2245 * 'lgtm' - Code-Review +2 from at least one approved reviewer
2246 * 'commit' - in the commit queue
2247 * 'closed' - abandoned
2248 """
2249 if not self.GetIssue():
2250 return None
2251
2252 try:
2253 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
2254 except httplib.HTTPException:
2255 return 'error'
2256
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002257 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002258 return 'closed'
2259
2260 cq_label = data['labels'].get('Commit-Queue', {})
2261 if cq_label:
2262 # Vote value is a stringified integer, which we expect from 0 to 2.
2263 vote_value = cq_label.get('value', '0')
2264 vote_text = cq_label.get('values', {}).get(vote_value, '')
2265 if vote_text.lower() == 'commit':
2266 return 'commit'
2267
2268 lgtm_label = data['labels'].get('Code-Review', {})
2269 if lgtm_label:
2270 if 'rejected' in lgtm_label:
2271 return 'not lgtm'
2272 if 'approved' in lgtm_label:
2273 return 'lgtm'
2274
2275 if not data.get('reviewers', {}).get('REVIEWER', []):
2276 return 'unsent'
2277
2278 messages = data.get('messages', [])
2279 if messages:
2280 owner = data['owner'].get('_account_id')
2281 last_message_author = messages[-1].get('author', {}).get('_account_id')
2282 if owner != last_message_author:
2283 # Some reply from non-owner.
2284 return 'reply'
2285
2286 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002287
2288 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002289 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002290 return data['revisions'][data['current_revision']]['_number']
2291
2292 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002293 data = self._GetChangeDetail(['CURRENT_REVISION'])
2294 current_rev = data['current_revision']
2295 url = data['revisions'][current_rev]['fetch']['http']['url']
2296 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002297
dsansomee2d6fd92016-09-08 00:10:47 -07002298 def UpdateDescriptionRemote(self, description, force=False):
2299 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2300 if not force:
2301 ask_for_data(
2302 'The description cannot be modified while the issue has a pending '
2303 'unpublished edit. Either publish the edit in the Gerrit web UI '
2304 'or delete it.\n\n'
2305 'Press Enter to delete the unpublished edit, Ctrl+C to abort.')
2306
2307 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2308 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002309 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
2310 description)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002311
2312 def CloseIssue(self):
2313 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2314
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002315 def GetApprovingReviewers(self):
2316 """Returns a list of reviewers approving the change.
2317
2318 Note: not necessarily committers.
2319 """
2320 raise NotImplementedError()
2321
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002322 def SubmitIssue(self, wait_for_merge=True):
2323 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2324 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002325
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002326 def _GetChangeDetail(self, options=None, issue=None):
2327 options = options or []
2328 issue = issue or self.GetIssue()
2329 assert issue, 'issue required to query Gerrit'
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002330 return gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2331 options)
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002332
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002333 def CMDLand(self, force, bypass_hooks, verbose):
2334 if git_common.is_dirty_git_tree('land'):
2335 return 1
tandriid60367b2016-06-22 05:25:12 -07002336 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2337 if u'Commit-Queue' in detail.get('labels', {}):
2338 if not force:
2339 ask_for_data('\nIt seems this repository has a Commit Queue, '
2340 'which can test and land changes for you. '
2341 'Are you sure you wish to bypass it?\n'
2342 'Press Enter to continue, Ctrl+C to abort.')
2343
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002344 differs = True
tandriic4344b52016-08-29 06:04:54 -07002345 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002346 # Note: git diff outputs nothing if there is no diff.
2347 if not last_upload or RunGit(['diff', last_upload]).strip():
2348 print('WARNING: some changes from local branch haven\'t been uploaded')
2349 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002350 if detail['current_revision'] == last_upload:
2351 differs = False
2352 else:
2353 print('WARNING: local branch contents differ from latest uploaded '
2354 'patchset')
2355 if differs:
2356 if not force:
2357 ask_for_data(
2358 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2359 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2360 elif not bypass_hooks:
2361 hook_results = self.RunHook(
2362 committing=True,
2363 may_prompt=not force,
2364 verbose=verbose,
2365 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2366 if not hook_results.should_continue():
2367 return 1
2368
2369 self.SubmitIssue(wait_for_merge=True)
2370 print('Issue %s has been submitted.' % self.GetIssueURL())
2371 return 0
2372
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002373 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2374 directory):
2375 assert not reject
2376 assert not nocommit
2377 assert not directory
2378 assert parsed_issue_arg.valid
2379
2380 self._changelist.issue = parsed_issue_arg.issue
2381
2382 if parsed_issue_arg.hostname:
2383 self._gerrit_host = parsed_issue_arg.hostname
2384 self._gerrit_server = 'https://%s' % self._gerrit_host
2385
2386 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2387
2388 if not parsed_issue_arg.patchset:
2389 # Use current revision by default.
2390 revision_info = detail['revisions'][detail['current_revision']]
2391 patchset = int(revision_info['_number'])
2392 else:
2393 patchset = parsed_issue_arg.patchset
2394 for revision_info in detail['revisions'].itervalues():
2395 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2396 break
2397 else:
2398 DieWithError('Couldn\'t find patchset %i in issue %i' %
2399 (parsed_issue_arg.patchset, self.GetIssue()))
2400
2401 fetch_info = revision_info['fetch']['http']
2402 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2403 RunGit(['cherry-pick', 'FETCH_HEAD'])
2404 self.SetIssue(self.GetIssue())
2405 self.SetPatchset(patchset)
2406 print('Committed patch for issue %i pathset %i locally' %
2407 (self.GetIssue(), self.GetPatchset()))
2408 return 0
2409
2410 @staticmethod
2411 def ParseIssueURL(parsed_url):
2412 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2413 return None
2414 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2415 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2416 # Short urls like https://domain/<issue_number> can be used, but don't allow
2417 # specifying the patchset (you'd 404), but we allow that here.
2418 if parsed_url.path == '/':
2419 part = parsed_url.fragment
2420 else:
2421 part = parsed_url.path
2422 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2423 if match:
2424 return _ParsedIssueNumberArgument(
2425 issue=int(match.group(2)),
2426 patchset=int(match.group(4)) if match.group(4) else None,
2427 hostname=parsed_url.netloc)
2428 return None
2429
tandrii16e0b4e2016-06-07 10:34:28 -07002430 def _GerritCommitMsgHookCheck(self, offer_removal):
2431 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2432 if not os.path.exists(hook):
2433 return
2434 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2435 # custom developer made one.
2436 data = gclient_utils.FileRead(hook)
2437 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2438 return
2439 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002440 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002441 'and may interfere with it in subtle ways.\n'
2442 'We recommend you remove the commit-msg hook.')
2443 if offer_removal:
2444 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2445 if reply.lower().startswith('y'):
2446 gclient_utils.rm_file_or_tree(hook)
2447 print('Gerrit commit-msg hook removed.')
2448 else:
2449 print('OK, will keep Gerrit commit-msg hook in place.')
2450
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002451 def CMDUploadChange(self, options, args, change):
2452 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002453 if options.squash and options.no_squash:
2454 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002455
2456 if not options.squash and not options.no_squash:
2457 # Load default for user, repo, squash=true, in this order.
2458 options.squash = settings.GetSquashGerritUploads()
2459 elif options.no_squash:
2460 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002461
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002462 # We assume the remote called "origin" is the one we want.
2463 # It is probably not worthwhile to support different workflows.
2464 gerrit_remote = 'origin'
2465
2466 remote, remote_branch = self.GetRemoteBranch()
2467 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2468 pending_prefix='')
2469
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002470 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002471 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002472 if self.GetIssue():
2473 # Try to get the message from a previous upload.
2474 message = self.GetDescription()
2475 if not message:
2476 DieWithError(
2477 'failed to fetch description from current Gerrit issue %d\n'
2478 '%s' % (self.GetIssue(), self.GetIssueURL()))
2479 change_id = self._GetChangeDetail()['change_id']
2480 while True:
2481 footer_change_ids = git_footers.get_footer_change_id(message)
2482 if footer_change_ids == [change_id]:
2483 break
2484 if not footer_change_ids:
2485 message = git_footers.add_footer_change_id(message, change_id)
2486 print('WARNING: appended missing Change-Id to issue description')
2487 continue
2488 # There is already a valid footer but with different or several ids.
2489 # Doing this automatically is non-trivial as we don't want to lose
2490 # existing other footers, yet we want to append just 1 desired
2491 # Change-Id. Thus, just create a new footer, but let user verify the
2492 # new description.
2493 message = '%s\n\nChange-Id: %s' % (message, change_id)
2494 print(
2495 'WARNING: issue %s has Change-Id footer(s):\n'
2496 ' %s\n'
2497 'but issue has Change-Id %s, according to Gerrit.\n'
2498 'Please, check the proposed correction to the description, '
2499 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2500 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2501 change_id))
2502 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2503 if not options.force:
2504 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002505 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002506 message = change_desc.description
2507 if not message:
2508 DieWithError("Description is empty. Aborting...")
2509 # Continue the while loop.
2510 # Sanity check of this code - we should end up with proper message
2511 # footer.
2512 assert [change_id] == git_footers.get_footer_change_id(message)
2513 change_desc = ChangeDescription(message)
2514 else:
2515 change_desc = ChangeDescription(
2516 options.message or CreateDescriptionFromLog(args))
2517 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002518 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002519 if not change_desc.description:
2520 DieWithError("Description is empty. Aborting...")
2521 message = change_desc.description
2522 change_ids = git_footers.get_footer_change_id(message)
2523 if len(change_ids) > 1:
2524 DieWithError('too many Change-Id footers, at most 1 allowed.')
2525 if not change_ids:
2526 # Generate the Change-Id automatically.
2527 message = git_footers.add_footer_change_id(
2528 message, GenerateGerritChangeId(message))
2529 change_desc.set_description(message)
2530 change_ids = git_footers.get_footer_change_id(message)
2531 assert len(change_ids) == 1
2532 change_id = change_ids[0]
2533
2534 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2535 if remote is '.':
2536 # If our upstream branch is local, we base our squashed commit on its
2537 # squashed version.
2538 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2539 # Check the squashed hash of the parent.
2540 parent = RunGit(['config',
2541 'branch.%s.gerritsquashhash' % upstream_branch_name],
2542 error_ok=True).strip()
2543 # Verify that the upstream branch has been uploaded too, otherwise
2544 # Gerrit will create additional CLs when uploading.
2545 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2546 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002547 DieWithError(
2548 'Upload upstream branch %s first.\n'
tandrii2bdadf12016-07-12 12:27:54 -07002549 'Note: maybe you\'ve uploaded it with --no-squash. '
2550 'If so, then re-upload it with:\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002551 ' git cl upload --squash\n' % upstream_branch_name)
2552 else:
2553 parent = self.GetCommonAncestorWithUpstream()
2554
2555 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2556 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2557 '-m', message]).strip()
2558 else:
2559 change_desc = ChangeDescription(
2560 options.message or CreateDescriptionFromLog(args))
2561 if not change_desc.description:
2562 DieWithError("Description is empty. Aborting...")
2563
2564 if not git_footers.get_footer_change_id(change_desc.description):
2565 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002566 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2567 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002568 ref_to_push = 'HEAD'
2569 parent = '%s/%s' % (gerrit_remote, branch)
2570 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2571
2572 assert change_desc
2573 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2574 ref_to_push)]).splitlines()
2575 if len(commits) > 1:
2576 print('WARNING: This will upload %d commits. Run the following command '
2577 'to see which commits will be uploaded: ' % len(commits))
2578 print('git log %s..%s' % (parent, ref_to_push))
2579 print('You can also use `git squash-branch` to squash these into a '
2580 'single commit.')
2581 ask_for_data('About to upload; enter to confirm.')
2582
2583 if options.reviewers or options.tbr_owners:
2584 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2585 change)
2586
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002587 # Extra options that can be specified at push time. Doc:
2588 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2589 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002590 if change_desc.get_reviewers(tbr_only=True):
2591 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2592 refspec_opts.append('l=Code-Review+1')
2593
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002594 if options.title:
tandriieefe8322016-08-17 10:12:24 -07002595 if not re.match(r'^[\w ]+$', options.title):
2596 options.title = re.sub(r'[^\w ]', '', options.title)
2597 print('WARNING: Patchset title may only contain alphanumeric chars '
2598 'and spaces. Cleaned up title:\n%s' % options.title)
2599 if not options.force:
2600 ask_for_data('Press enter to continue, Ctrl+C to abort')
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002601 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2602 # reverse on its side.
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002603 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2604
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002605 if options.send_mail:
2606 if not change_desc.get_reviewers():
2607 DieWithError('Must specify reviewers to send email.')
2608 refspec_opts.append('notify=ALL')
2609 else:
2610 refspec_opts.append('notify=NONE')
2611
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002612 cc = self.GetCCList().split(',')
2613 if options.cc:
2614 cc.extend(options.cc)
2615 cc = filter(None, cc)
2616 if cc:
tandrii@chromium.org074c2af2016-06-03 23:18:40 +00002617 refspec_opts.extend('cc=' + email.strip() for email in cc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002618
tandrii99a72f22016-08-17 14:33:24 -07002619 reviewers = change_desc.get_reviewers()
2620 if reviewers:
2621 refspec_opts.extend('r=' + email.strip() for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002622
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002623 refspec_suffix = ''
2624 if refspec_opts:
2625 refspec_suffix = '%' + ','.join(refspec_opts)
2626 assert ' ' not in refspec_suffix, (
2627 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002628 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002629
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002630 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002631 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002632 print_stdout=True,
2633 # Flush after every line: useful for seeing progress when running as
2634 # recipe.
2635 filter_fn=lambda _: sys.stdout.flush())
2636
2637 if options.squash:
2638 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2639 change_numbers = [m.group(1)
2640 for m in map(regex.match, push_stdout.splitlines())
2641 if m]
2642 if len(change_numbers) != 1:
2643 DieWithError(
2644 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2645 'Change-Id: %s') % (len(change_numbers), change_id))
2646 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002647 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002648 return 0
2649
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002650 def _AddChangeIdToCommitMessage(self, options, args):
2651 """Re-commits using the current message, assumes the commit hook is in
2652 place.
2653 """
2654 log_desc = options.message or CreateDescriptionFromLog(args)
2655 git_command = ['commit', '--amend', '-m', log_desc]
2656 RunGit(git_command)
2657 new_log_desc = CreateDescriptionFromLog(args)
2658 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002659 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002660 return new_log_desc
2661 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002662 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002663
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002664 def SetCQState(self, new_state):
2665 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002666 vote_map = {
2667 _CQState.NONE: 0,
2668 _CQState.DRY_RUN: 1,
2669 _CQState.COMMIT : 2,
2670 }
2671 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2672 labels={'Commit-Queue': vote_map[new_state]})
2673
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002674
2675_CODEREVIEW_IMPLEMENTATIONS = {
2676 'rietveld': _RietveldChangelistImpl,
2677 'gerrit': _GerritChangelistImpl,
2678}
2679
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002680
iannuccie53c9352016-08-17 14:40:40 -07002681def _add_codereview_issue_select_options(parser, extra=""):
2682 _add_codereview_select_options(parser)
2683
2684 text = ('Operate on this issue number instead of the current branch\'s '
2685 'implicit issue.')
2686 if extra:
2687 text += ' '+extra
2688 parser.add_option('-i', '--issue', type=int, help=text)
2689
2690
2691def _process_codereview_issue_select_options(parser, options):
2692 _process_codereview_select_options(parser, options)
2693 if options.issue is not None and not options.forced_codereview:
2694 parser.error('--issue must be specified with either --rietveld or --gerrit')
2695
2696
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002697def _add_codereview_select_options(parser):
2698 """Appends --gerrit and --rietveld options to force specific codereview."""
2699 parser.codereview_group = optparse.OptionGroup(
2700 parser, 'EXPERIMENTAL! Codereview override options')
2701 parser.add_option_group(parser.codereview_group)
2702 parser.codereview_group.add_option(
2703 '--gerrit', action='store_true',
2704 help='Force the use of Gerrit for codereview')
2705 parser.codereview_group.add_option(
2706 '--rietveld', action='store_true',
2707 help='Force the use of Rietveld for codereview')
2708
2709
2710def _process_codereview_select_options(parser, options):
2711 if options.gerrit and options.rietveld:
2712 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2713 options.forced_codereview = None
2714 if options.gerrit:
2715 options.forced_codereview = 'gerrit'
2716 elif options.rietveld:
2717 options.forced_codereview = 'rietveld'
2718
2719
tandriif9aefb72016-07-01 09:06:51 -07002720def _get_bug_line_values(default_project, bugs):
2721 """Given default_project and comma separated list of bugs, yields bug line
2722 values.
2723
2724 Each bug can be either:
2725 * a number, which is combined with default_project
2726 * string, which is left as is.
2727
2728 This function may produce more than one line, because bugdroid expects one
2729 project per line.
2730
2731 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2732 ['v8:123', 'chromium:789']
2733 """
2734 default_bugs = []
2735 others = []
2736 for bug in bugs.split(','):
2737 bug = bug.strip()
2738 if bug:
2739 try:
2740 default_bugs.append(int(bug))
2741 except ValueError:
2742 others.append(bug)
2743
2744 if default_bugs:
2745 default_bugs = ','.join(map(str, default_bugs))
2746 if default_project:
2747 yield '%s:%s' % (default_project, default_bugs)
2748 else:
2749 yield default_bugs
2750 for other in sorted(others):
2751 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2752 yield other
2753
2754
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002755class ChangeDescription(object):
2756 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002757 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002758 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002759
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002760 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002761 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002762
agable@chromium.org42c20792013-09-12 17:34:49 +00002763 @property # www.logilab.org/ticket/89786
2764 def description(self): # pylint: disable=E0202
2765 return '\n'.join(self._description_lines)
2766
2767 def set_description(self, desc):
2768 if isinstance(desc, basestring):
2769 lines = desc.splitlines()
2770 else:
2771 lines = [line.rstrip() for line in desc]
2772 while lines and not lines[0]:
2773 lines.pop(0)
2774 while lines and not lines[-1]:
2775 lines.pop(-1)
2776 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002777
piman@chromium.org336f9122014-09-04 02:16:55 +00002778 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002779 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002780 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002781 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002782 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002783 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002784
agable@chromium.org42c20792013-09-12 17:34:49 +00002785 # Get the set of R= and TBR= lines and remove them from the desciption.
2786 regexp = re.compile(self.R_LINE)
2787 matches = [regexp.match(line) for line in self._description_lines]
2788 new_desc = [l for i, l in enumerate(self._description_lines)
2789 if not matches[i]]
2790 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002791
agable@chromium.org42c20792013-09-12 17:34:49 +00002792 # Construct new unified R= and TBR= lines.
2793 r_names = []
2794 tbr_names = []
2795 for match in matches:
2796 if not match:
2797 continue
2798 people = cleanup_list([match.group(2).strip()])
2799 if match.group(1) == 'TBR':
2800 tbr_names.extend(people)
2801 else:
2802 r_names.extend(people)
2803 for name in r_names:
2804 if name not in reviewers:
2805 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002806 if add_owners_tbr:
2807 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07002808 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00002809 all_reviewers = set(tbr_names + reviewers)
2810 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2811 all_reviewers)
2812 tbr_names.extend(owners_db.reviewers_for(missing_files,
2813 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002814 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2815 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2816
2817 # Put the new lines in the description where the old first R= line was.
2818 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2819 if 0 <= line_loc < len(self._description_lines):
2820 if new_tbr_line:
2821 self._description_lines.insert(line_loc, new_tbr_line)
2822 if new_r_line:
2823 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002824 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002825 if new_r_line:
2826 self.append_footer(new_r_line)
2827 if new_tbr_line:
2828 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002829
tandriif9aefb72016-07-01 09:06:51 -07002830 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002831 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002832 self.set_description([
2833 '# Enter a description of the change.',
2834 '# This will be displayed on the codereview site.',
2835 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002836 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002837 '--------------------',
2838 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002839
agable@chromium.org42c20792013-09-12 17:34:49 +00002840 regexp = re.compile(self.BUG_LINE)
2841 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07002842 prefix = settings.GetBugPrefix()
2843 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
2844 for value in values:
2845 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
2846 self.append_footer('BUG=%s' % value)
2847
agable@chromium.org42c20792013-09-12 17:34:49 +00002848 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002849 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002850 if not content:
2851 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002852 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002853
2854 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002855 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2856 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002857 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002858 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002859
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002860 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00002861 """Adds a footer line to the description.
2862
2863 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
2864 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
2865 that Gerrit footers are always at the end.
2866 """
2867 parsed_footer_line = git_footers.parse_footer(line)
2868 if parsed_footer_line:
2869 # Line is a gerrit footer in the form: Footer-Key: any value.
2870 # Thus, must be appended observing Gerrit footer rules.
2871 self.set_description(
2872 git_footers.add_footer(self.description,
2873 key=parsed_footer_line[0],
2874 value=parsed_footer_line[1]))
2875 return
2876
2877 if not self._description_lines:
2878 self._description_lines.append(line)
2879 return
2880
2881 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
2882 if gerrit_footers:
2883 # git_footers.split_footers ensures that there is an empty line before
2884 # actual (gerrit) footers, if any. We have to keep it that way.
2885 assert top_lines and top_lines[-1] == ''
2886 top_lines, separator = top_lines[:-1], top_lines[-1:]
2887 else:
2888 separator = [] # No need for separator if there are no gerrit_footers.
2889
2890 prev_line = top_lines[-1] if top_lines else ''
2891 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
2892 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2893 top_lines.append('')
2894 top_lines.append(line)
2895 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002896
tandrii99a72f22016-08-17 14:33:24 -07002897 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002898 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002899 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07002900 reviewers = [match.group(2).strip()
2901 for match in matches
2902 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002903 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002904
2905
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002906def get_approving_reviewers(props):
2907 """Retrieves the reviewers that approved a CL from the issue properties with
2908 messages.
2909
2910 Note that the list may contain reviewers that are not committer, thus are not
2911 considered by the CQ.
2912 """
2913 return sorted(
2914 set(
2915 message['sender']
2916 for message in props['messages']
2917 if message['approval'] and message['sender'] in props['reviewers']
2918 )
2919 )
2920
2921
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002922def FindCodereviewSettingsFile(filename='codereview.settings'):
2923 """Finds the given file starting in the cwd and going up.
2924
2925 Only looks up to the top of the repository unless an
2926 'inherit-review-settings-ok' file exists in the root of the repository.
2927 """
2928 inherit_ok_file = 'inherit-review-settings-ok'
2929 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002930 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002931 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2932 root = '/'
2933 while True:
2934 if filename in os.listdir(cwd):
2935 if os.path.isfile(os.path.join(cwd, filename)):
2936 return open(os.path.join(cwd, filename))
2937 if cwd == root:
2938 break
2939 cwd = os.path.dirname(cwd)
2940
2941
2942def LoadCodereviewSettingsFromFile(fileobj):
2943 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002944 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002945
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002946 def SetProperty(name, setting, unset_error_ok=False):
2947 fullname = 'rietveld.' + name
2948 if setting in keyvals:
2949 RunGit(['config', fullname, keyvals[setting]])
2950 else:
2951 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2952
2953 SetProperty('server', 'CODE_REVIEW_SERVER')
2954 # Only server setting is required. Other settings can be absent.
2955 # In that case, we ignore errors raised during option deletion attempt.
2956 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002957 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002958 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2959 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002960 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002961 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002962 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2963 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002964 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002965 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002966 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002967 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2968 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002969
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002970 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002971 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002972
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002973 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07002974 RunGit(['config', 'gerrit.squash-uploads',
2975 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002976
tandrii@chromium.org28253532016-04-14 13:46:56 +00002977 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00002978 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00002979 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
2980
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002981 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2982 #should be of the form
2983 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2984 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2985 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2986 keyvals['ORIGIN_URL_CONFIG']])
2987
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002988
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002989def urlretrieve(source, destination):
2990 """urllib is broken for SSL connections via a proxy therefore we
2991 can't use urllib.urlretrieve()."""
2992 with open(destination, 'w') as f:
2993 f.write(urllib2.urlopen(source).read())
2994
2995
ukai@chromium.org712d6102013-11-27 00:52:58 +00002996def hasSheBang(fname):
2997 """Checks fname is a #! script."""
2998 with open(fname) as f:
2999 return f.read(2).startswith('#!')
3000
3001
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003002# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3003def DownloadHooks(*args, **kwargs):
3004 pass
3005
3006
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003007def DownloadGerritHook(force):
3008 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003009
3010 Args:
3011 force: True to update hooks. False to install hooks if not present.
3012 """
3013 if not settings.GetIsGerrit():
3014 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003015 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003016 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3017 if not os.access(dst, os.X_OK):
3018 if os.path.exists(dst):
3019 if not force:
3020 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003021 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003022 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003023 if not hasSheBang(dst):
3024 DieWithError('Not a script: %s\n'
3025 'You need to download from\n%s\n'
3026 'into .git/hooks/commit-msg and '
3027 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003028 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3029 except Exception:
3030 if os.path.exists(dst):
3031 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003032 DieWithError('\nFailed to download hooks.\n'
3033 'You need to download from\n%s\n'
3034 'into .git/hooks/commit-msg and '
3035 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003036
3037
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003038
3039def GetRietveldCodereviewSettingsInteractively():
3040 """Prompt the user for settings."""
3041 server = settings.GetDefaultServerUrl(error_ok=True)
3042 prompt = 'Rietveld server (host[:port])'
3043 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3044 newserver = ask_for_data(prompt + ':')
3045 if not server and not newserver:
3046 newserver = DEFAULT_SERVER
3047 if newserver:
3048 newserver = gclient_utils.UpgradeToHttps(newserver)
3049 if newserver != server:
3050 RunGit(['config', 'rietveld.server', newserver])
3051
3052 def SetProperty(initial, caption, name, is_url):
3053 prompt = caption
3054 if initial:
3055 prompt += ' ("x" to clear) [%s]' % initial
3056 new_val = ask_for_data(prompt + ':')
3057 if new_val == 'x':
3058 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3059 elif new_val:
3060 if is_url:
3061 new_val = gclient_utils.UpgradeToHttps(new_val)
3062 if new_val != initial:
3063 RunGit(['config', 'rietveld.' + name, new_val])
3064
3065 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3066 SetProperty(settings.GetDefaultPrivateFlag(),
3067 'Private flag (rietveld only)', 'private', False)
3068 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3069 'tree-status-url', False)
3070 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3071 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3072 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3073 'run-post-upload-hook', False)
3074
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003075@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003076def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003077 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003078
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003079 print('WARNING: git cl config works for Rietveld only.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003080 'For Gerrit, see http://crbug.com/603116.')
3081 # TODO(tandrii): add Gerrit support as part of http://crbug.com/603116.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003082 parser.add_option('--activate-update', action='store_true',
3083 help='activate auto-updating [rietveld] section in '
3084 '.git/config')
3085 parser.add_option('--deactivate-update', action='store_true',
3086 help='deactivate auto-updating [rietveld] section in '
3087 '.git/config')
3088 options, args = parser.parse_args(args)
3089
3090 if options.deactivate_update:
3091 RunGit(['config', 'rietveld.autoupdate', 'false'])
3092 return
3093
3094 if options.activate_update:
3095 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3096 return
3097
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003098 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003099 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003100 return 0
3101
3102 url = args[0]
3103 if not url.endswith('codereview.settings'):
3104 url = os.path.join(url, 'codereview.settings')
3105
3106 # Load code review settings and download hooks (if available).
3107 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3108 return 0
3109
3110
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003111def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003112 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003113 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3114 branch = ShortBranchName(branchref)
3115 _, args = parser.parse_args(args)
3116 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003117 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003118 return RunGit(['config', 'branch.%s.base-url' % branch],
3119 error_ok=False).strip()
3120 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003121 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003122 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3123 error_ok=False).strip()
3124
3125
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003126def color_for_status(status):
3127 """Maps a Changelist status to color, for CMDstatus and other tools."""
3128 return {
3129 'unsent': Fore.RED,
3130 'waiting': Fore.BLUE,
3131 'reply': Fore.YELLOW,
3132 'lgtm': Fore.GREEN,
3133 'commit': Fore.MAGENTA,
3134 'closed': Fore.CYAN,
3135 'error': Fore.WHITE,
3136 }.get(status, Fore.WHITE)
3137
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003138
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003139def get_cl_statuses(changes, fine_grained, max_processes=None):
3140 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003141
3142 If fine_grained is true, this will fetch CL statuses from the server.
3143 Otherwise, simply indicate if there's a matching url for the given branches.
3144
3145 If max_processes is specified, it is used as the maximum number of processes
3146 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3147 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003148
3149 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003150 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003151 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003152 upload.verbosity = 0
3153
3154 if fine_grained:
3155 # Process one branch synchronously to work through authentication, then
3156 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003157 if changes:
tandriiea9514a2016-08-17 12:32:37 -07003158 def fetch(cl):
3159 try:
3160 return (cl, cl.GetStatus())
3161 except:
3162 # See http://crbug.com/629863.
3163 logging.exception('failed to fetch status for %s:', cl)
3164 raise
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003165 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003166
tandriiea9514a2016-08-17 12:32:37 -07003167 changes_to_fetch = changes[1:]
3168 if not changes_to_fetch:
kmarshall3bff56b2016-06-06 18:31:47 -07003169 # Exit early if there was only one branch to fetch.
3170 return
3171
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003172 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003173 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003174 if max_processes is not None
dsinclair99d30172016-08-09 10:48:58 -07003175 else max(len(changes_to_fetch), 1))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003176
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003177 fetched_cls = set()
3178 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003179 while True:
3180 try:
3181 row = it.next(timeout=5)
3182 except multiprocessing.TimeoutError:
3183 break
3184
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003185 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003186 yield row
3187
3188 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003189 for cl in set(changes_to_fetch) - fetched_cls:
3190 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003191
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003192 else:
3193 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003194 for cl in changes:
3195 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003196
rmistry@google.com2dd99862015-06-22 12:22:18 +00003197
3198def upload_branch_deps(cl, args):
3199 """Uploads CLs of local branches that are dependents of the current branch.
3200
3201 If the local branch dependency tree looks like:
3202 test1 -> test2.1 -> test3.1
3203 -> test3.2
3204 -> test2.2 -> test3.3
3205
3206 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3207 run on the dependent branches in this order:
3208 test2.1, test3.1, test3.2, test2.2, test3.3
3209
3210 Note: This function does not rebase your local dependent branches. Use it when
3211 you make a change to the parent branch that will not conflict with its
3212 dependent branches, and you would like their dependencies updated in
3213 Rietveld.
3214 """
3215 if git_common.is_dirty_git_tree('upload-branch-deps'):
3216 return 1
3217
3218 root_branch = cl.GetBranch()
3219 if root_branch is None:
3220 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3221 'Get on a branch!')
3222 if not cl.GetIssue() or not cl.GetPatchset():
3223 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3224 'patchset dependencies without an uploaded CL.')
3225
3226 branches = RunGit(['for-each-ref',
3227 '--format=%(refname:short) %(upstream:short)',
3228 'refs/heads'])
3229 if not branches:
3230 print('No local branches found.')
3231 return 0
3232
3233 # Create a dictionary of all local branches to the branches that are dependent
3234 # on it.
3235 tracked_to_dependents = collections.defaultdict(list)
3236 for b in branches.splitlines():
3237 tokens = b.split()
3238 if len(tokens) == 2:
3239 branch_name, tracked = tokens
3240 tracked_to_dependents[tracked].append(branch_name)
3241
vapiera7fbd5a2016-06-16 09:17:49 -07003242 print()
3243 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003244 dependents = []
3245 def traverse_dependents_preorder(branch, padding=''):
3246 dependents_to_process = tracked_to_dependents.get(branch, [])
3247 padding += ' '
3248 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003249 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003250 dependents.append(dependent)
3251 traverse_dependents_preorder(dependent, padding)
3252 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003253 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003254
3255 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003256 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003257 return 0
3258
vapiera7fbd5a2016-06-16 09:17:49 -07003259 print('This command will checkout all dependent branches and run '
3260 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003261 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3262
andybons@chromium.org962f9462016-02-03 20:00:42 +00003263 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003264 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003265 args.extend(['-t', 'Updated patchset dependency'])
3266
rmistry@google.com2dd99862015-06-22 12:22:18 +00003267 # Record all dependents that failed to upload.
3268 failures = {}
3269 # Go through all dependents, checkout the branch and upload.
3270 try:
3271 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003272 print()
3273 print('--------------------------------------')
3274 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003275 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003276 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003277 try:
3278 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003279 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003280 failures[dependent_branch] = 1
3281 except: # pylint: disable=W0702
3282 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003283 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003284 finally:
3285 # Swap back to the original root branch.
3286 RunGit(['checkout', '-q', root_branch])
3287
vapiera7fbd5a2016-06-16 09:17:49 -07003288 print()
3289 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003290 for dependent_branch in dependents:
3291 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003292 print(' %s : %s' % (dependent_branch, upload_status))
3293 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003294
3295 return 0
3296
3297
kmarshall3bff56b2016-06-06 18:31:47 -07003298def CMDarchive(parser, args):
3299 """Archives and deletes branches associated with closed changelists."""
3300 parser.add_option(
3301 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003302 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003303 parser.add_option(
3304 '-f', '--force', action='store_true',
3305 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003306 parser.add_option(
3307 '-d', '--dry-run', action='store_true',
3308 help='Skip the branch tagging and removal steps.')
3309 parser.add_option(
3310 '-t', '--notags', action='store_true',
3311 help='Do not tag archived branches. '
3312 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003313
3314 auth.add_auth_options(parser)
3315 options, args = parser.parse_args(args)
3316 if args:
3317 parser.error('Unsupported args: %s' % ' '.join(args))
3318 auth_config = auth.extract_auth_config_from_options(options)
3319
3320 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3321 if not branches:
3322 return 0
3323
vapiera7fbd5a2016-06-16 09:17:49 -07003324 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003325 changes = [Changelist(branchref=b, auth_config=auth_config)
3326 for b in branches.splitlines()]
3327 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3328 statuses = get_cl_statuses(changes,
3329 fine_grained=True,
3330 max_processes=options.maxjobs)
3331 proposal = [(cl.GetBranch(),
3332 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3333 for cl, status in statuses
3334 if status == 'closed']
3335 proposal.sort()
3336
3337 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003338 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003339 return 0
3340
3341 current_branch = GetCurrentBranch()
3342
vapiera7fbd5a2016-06-16 09:17:49 -07003343 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003344 if options.notags:
3345 for next_item in proposal:
3346 print(' ' + next_item[0])
3347 else:
3348 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3349 for next_item in proposal:
3350 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003351
kmarshall9249e012016-08-23 12:02:16 -07003352 # Quit now on precondition failure or if instructed by the user, either
3353 # via an interactive prompt or by command line flags.
3354 if options.dry_run:
3355 print('\nNo changes were made (dry run).\n')
3356 return 0
3357 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003358 print('You are currently on a branch \'%s\' which is associated with a '
3359 'closed codereview issue, so archive cannot proceed. Please '
3360 'checkout another branch and run this command again.' %
3361 current_branch)
3362 return 1
kmarshall9249e012016-08-23 12:02:16 -07003363 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003364 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3365 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003366 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003367 return 1
3368
3369 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003370 if not options.notags:
3371 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003372 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003373
vapiera7fbd5a2016-06-16 09:17:49 -07003374 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003375
3376 return 0
3377
3378
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003379def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003380 """Show status of changelists.
3381
3382 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003383 - Red not sent for review or broken
3384 - Blue waiting for review
3385 - Yellow waiting for you to reply to review
3386 - Green LGTM'ed
3387 - Magenta in the commit queue
3388 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003389
3390 Also see 'git cl comments'.
3391 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003392 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003393 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003394 parser.add_option('-f', '--fast', action='store_true',
3395 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003396 parser.add_option(
3397 '-j', '--maxjobs', action='store', type=int,
3398 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003399
3400 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003401 _add_codereview_issue_select_options(
3402 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003403 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003404 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003405 if args:
3406 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003407 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003408
iannuccie53c9352016-08-17 14:40:40 -07003409 if options.issue is not None and not options.field:
3410 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003411
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003412 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003413 cl = Changelist(auth_config=auth_config, issue=options.issue,
3414 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003415 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003416 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003417 elif options.field == 'id':
3418 issueid = cl.GetIssue()
3419 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003420 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003421 elif options.field == 'patch':
3422 patchset = cl.GetPatchset()
3423 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003424 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003425 elif options.field == 'status':
3426 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003427 elif options.field == 'url':
3428 url = cl.GetIssueURL()
3429 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003430 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003431 return 0
3432
3433 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3434 if not branches:
3435 print('No local branch found.')
3436 return 0
3437
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003438 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003439 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003440 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003441 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003442 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003443 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003444 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003445
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003446 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003447 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3448 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3449 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003450 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003451 c, status = output.next()
3452 branch_statuses[c.GetBranch()] = status
3453 status = branch_statuses.pop(branch)
3454 url = cl.GetIssueURL()
3455 if url and (not status or status == 'error'):
3456 # The issue probably doesn't exist anymore.
3457 url += ' (broken)'
3458
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003459 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003460 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003461 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003462 color = ''
3463 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003464 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003465 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003466 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003467 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003468
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003469 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003470 print()
3471 print('Current branch:',)
3472 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003473 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003474 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003475 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003476 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003477 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003478 print('Issue description:')
3479 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003480 return 0
3481
3482
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003483def colorize_CMDstatus_doc():
3484 """To be called once in main() to add colors to git cl status help."""
3485 colors = [i for i in dir(Fore) if i[0].isupper()]
3486
3487 def colorize_line(line):
3488 for color in colors:
3489 if color in line.upper():
3490 # Extract whitespaces first and the leading '-'.
3491 indent = len(line) - len(line.lstrip(' ')) + 1
3492 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3493 return line
3494
3495 lines = CMDstatus.__doc__.splitlines()
3496 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3497
3498
phajdan.jre328cf92016-08-22 04:12:17 -07003499def write_json(path, contents):
3500 with open(path, 'w') as f:
3501 json.dump(contents, f)
3502
3503
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003504@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003505def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003506 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003507
3508 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003509 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003510 parser.add_option('-r', '--reverse', action='store_true',
3511 help='Lookup the branch(es) for the specified issues. If '
3512 'no issues are specified, all branches with mapped '
3513 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003514 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003515 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003516 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003517 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003518
dnj@chromium.org406c4402015-03-03 17:22:28 +00003519 if options.reverse:
3520 branches = RunGit(['for-each-ref', 'refs/heads',
3521 '--format=%(refname:short)']).splitlines()
3522
3523 # Reverse issue lookup.
3524 issue_branch_map = {}
3525 for branch in branches:
3526 cl = Changelist(branchref=branch)
3527 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3528 if not args:
3529 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003530 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003531 for issue in args:
3532 if not issue:
3533 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003534 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003535 print('Branch for issue number %s: %s' % (
3536 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003537 if options.json:
3538 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003539 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003540 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003541 if len(args) > 0:
3542 try:
3543 issue = int(args[0])
3544 except ValueError:
3545 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003546 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003547 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003548 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003549 if options.json:
3550 write_json(options.json, {
3551 'issue': cl.GetIssue(),
3552 'issue_url': cl.GetIssueURL(),
3553 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003554 return 0
3555
3556
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003557def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003558 """Shows or posts review comments for any changelist."""
3559 parser.add_option('-a', '--add-comment', dest='comment',
3560 help='comment to add to an issue')
3561 parser.add_option('-i', dest='issue',
3562 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003563 parser.add_option('-j', '--json-file',
3564 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003565 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003566 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003567 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003568
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003569 issue = None
3570 if options.issue:
3571 try:
3572 issue = int(options.issue)
3573 except ValueError:
3574 DieWithError('A review issue id is expected to be a number')
3575
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003576 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003577
3578 if options.comment:
3579 cl.AddComment(options.comment)
3580 return 0
3581
3582 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003583 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003584 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003585 summary.append({
3586 'date': message['date'],
3587 'lgtm': False,
3588 'message': message['text'],
3589 'not_lgtm': False,
3590 'sender': message['sender'],
3591 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003592 if message['disapproval']:
3593 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003594 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003595 elif message['approval']:
3596 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003597 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003598 elif message['sender'] == data['owner_email']:
3599 color = Fore.MAGENTA
3600 else:
3601 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003602 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003603 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003604 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003605 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003606 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003607 if options.json_file:
3608 with open(options.json_file, 'wb') as f:
3609 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003610 return 0
3611
3612
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003613@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003614def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003615 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003616 parser.add_option('-d', '--display', action='store_true',
3617 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003618 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07003619 help='New description to set for this issue (- for stdin, '
3620 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07003621 parser.add_option('-f', '--force', action='store_true',
3622 help='Delete any unpublished Gerrit edits for this issue '
3623 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003624
3625 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003626 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003627 options, args = parser.parse_args(args)
3628 _process_codereview_select_options(parser, options)
3629
3630 target_issue = None
3631 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07003632 target_issue = ParseIssueNumberArgument(args[0])
3633 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003634 parser.print_help()
3635 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003636
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003637 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003638
martiniss6eda05f2016-06-30 10:18:35 -07003639 kwargs = {
3640 'auth_config': auth_config,
3641 'codereview': options.forced_codereview,
3642 }
3643 if target_issue:
3644 kwargs['issue'] = target_issue.issue
3645 if options.forced_codereview == 'rietveld':
3646 kwargs['rietveld_server'] = target_issue.hostname
3647
3648 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003649
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003650 if not cl.GetIssue():
3651 DieWithError('This branch has no associated changelist.')
3652 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003653
smut@google.com34fb6b12015-07-13 20:03:26 +00003654 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003655 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003656 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003657
3658 if options.new_description:
3659 text = options.new_description
3660 if text == '-':
3661 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07003662 elif text == '+':
3663 base_branch = cl.GetCommonAncestorWithUpstream()
3664 change = cl.GetChange(base_branch, None, local_description=True)
3665 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003666
3667 description.set_description(text)
3668 else:
3669 description.prompt()
3670
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003671 if cl.GetDescription() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07003672 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003673 return 0
3674
3675
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003676def CreateDescriptionFromLog(args):
3677 """Pulls out the commit log to use as a base for the CL description."""
3678 log_args = []
3679 if len(args) == 1 and not args[0].endswith('.'):
3680 log_args = [args[0] + '..']
3681 elif len(args) == 1 and args[0].endswith('...'):
3682 log_args = [args[0][:-1]]
3683 elif len(args) == 2:
3684 log_args = [args[0] + '..' + args[1]]
3685 else:
3686 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003687 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003688
3689
thestig@chromium.org44202a22014-03-11 19:22:18 +00003690def CMDlint(parser, args):
3691 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003692 parser.add_option('--filter', action='append', metavar='-x,+y',
3693 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003694 auth.add_auth_options(parser)
3695 options, args = parser.parse_args(args)
3696 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003697
3698 # Access to a protected member _XX of a client class
3699 # pylint: disable=W0212
3700 try:
3701 import cpplint
3702 import cpplint_chromium
3703 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003704 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003705 return 1
3706
3707 # Change the current working directory before calling lint so that it
3708 # shows the correct base.
3709 previous_cwd = os.getcwd()
3710 os.chdir(settings.GetRoot())
3711 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003712 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003713 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3714 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003715 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003716 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003717 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003718
3719 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003720 command = args + files
3721 if options.filter:
3722 command = ['--filter=' + ','.join(options.filter)] + command
3723 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003724
3725 white_regex = re.compile(settings.GetLintRegex())
3726 black_regex = re.compile(settings.GetLintIgnoreRegex())
3727 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3728 for filename in filenames:
3729 if white_regex.match(filename):
3730 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07003731 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003732 else:
3733 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3734 extra_check_functions)
3735 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003736 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003737 finally:
3738 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07003739 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003740 if cpplint._cpplint_state.error_count != 0:
3741 return 1
3742 return 0
3743
3744
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003745def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003746 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003747 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003748 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003749 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003750 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003751 auth.add_auth_options(parser)
3752 options, args = parser.parse_args(args)
3753 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003754
sbc@chromium.org71437c02015-04-09 19:29:40 +00003755 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07003756 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003757 return 1
3758
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003759 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003760 if args:
3761 base_branch = args[0]
3762 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003763 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003764 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003765
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003766 cl.RunHook(
3767 committing=not options.upload,
3768 may_prompt=False,
3769 verbose=options.verbose,
3770 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003771 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003772
3773
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003774def GenerateGerritChangeId(message):
3775 """Returns Ixxxxxx...xxx change id.
3776
3777 Works the same way as
3778 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3779 but can be called on demand on all platforms.
3780
3781 The basic idea is to generate git hash of a state of the tree, original commit
3782 message, author/committer info and timestamps.
3783 """
3784 lines = []
3785 tree_hash = RunGitSilent(['write-tree'])
3786 lines.append('tree %s' % tree_hash.strip())
3787 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3788 if code == 0:
3789 lines.append('parent %s' % parent.strip())
3790 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3791 lines.append('author %s' % author.strip())
3792 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3793 lines.append('committer %s' % committer.strip())
3794 lines.append('')
3795 # Note: Gerrit's commit-hook actually cleans message of some lines and
3796 # whitespace. This code is not doing this, but it clearly won't decrease
3797 # entropy.
3798 lines.append(message)
3799 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3800 stdin='\n'.join(lines))
3801 return 'I%s' % change_hash.strip()
3802
3803
wittman@chromium.org455dc922015-01-26 20:15:50 +00003804def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3805 """Computes the remote branch ref to use for the CL.
3806
3807 Args:
3808 remote (str): The git remote for the CL.
3809 remote_branch (str): The git remote branch for the CL.
3810 target_branch (str): The target branch specified by the user.
3811 pending_prefix (str): The pending prefix from the settings.
3812 """
3813 if not (remote and remote_branch):
3814 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003815
wittman@chromium.org455dc922015-01-26 20:15:50 +00003816 if target_branch:
3817 # Cannonicalize branch references to the equivalent local full symbolic
3818 # refs, which are then translated into the remote full symbolic refs
3819 # below.
3820 if '/' not in target_branch:
3821 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3822 else:
3823 prefix_replacements = (
3824 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3825 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3826 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3827 )
3828 match = None
3829 for regex, replacement in prefix_replacements:
3830 match = re.search(regex, target_branch)
3831 if match:
3832 remote_branch = target_branch.replace(match.group(0), replacement)
3833 break
3834 if not match:
3835 # This is a branch path but not one we recognize; use as-is.
3836 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003837 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3838 # Handle the refs that need to land in different refs.
3839 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003840
wittman@chromium.org455dc922015-01-26 20:15:50 +00003841 # Create the true path to the remote branch.
3842 # Does the following translation:
3843 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3844 # * refs/remotes/origin/master -> refs/heads/master
3845 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3846 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3847 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3848 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3849 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3850 'refs/heads/')
3851 elif remote_branch.startswith('refs/remotes/branch-heads'):
3852 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3853 # If a pending prefix exists then replace refs/ with it.
3854 if pending_prefix:
3855 remote_branch = remote_branch.replace('refs/', pending_prefix)
3856 return remote_branch
3857
3858
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003859def cleanup_list(l):
3860 """Fixes a list so that comma separated items are put as individual items.
3861
3862 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3863 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3864 """
3865 items = sum((i.split(',') for i in l), [])
3866 stripped_items = (i.strip() for i in items)
3867 return sorted(filter(None, stripped_items))
3868
3869
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003870@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003871def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003872 """Uploads the current changelist to codereview.
3873
3874 Can skip dependency patchset uploads for a branch by running:
3875 git config branch.branch_name.skip-deps-uploads True
3876 To unset run:
3877 git config --unset branch.branch_name.skip-deps-uploads
3878 Can also set the above globally by using the --global flag.
3879 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003880 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3881 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003882 parser.add_option('--bypass-watchlists', action='store_true',
3883 dest='bypass_watchlists',
3884 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003885 parser.add_option('-f', action='store_true', dest='force',
3886 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003887 parser.add_option('-m', dest='message', help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07003888 parser.add_option('-b', '--bug',
3889 help='pre-populate the bug number(s) for this issue. '
3890 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07003891 parser.add_option('--message-file', dest='message_file',
3892 help='file which contains message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003893 parser.add_option('-t', dest='title',
3894 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003895 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003896 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003897 help='reviewer email addresses')
3898 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003899 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003900 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003901 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003902 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003903 parser.add_option('--emulate_svn_auto_props',
3904 '--emulate-svn-auto-props',
3905 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003906 dest="emulate_svn_auto_props",
3907 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003908 parser.add_option('-c', '--use-commit-queue', action='store_true',
3909 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003910 parser.add_option('--private', action='store_true',
3911 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003912 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003913 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003914 metavar='TARGET',
3915 help='Apply CL to remote ref TARGET. ' +
3916 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003917 parser.add_option('--squash', action='store_true',
3918 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003919 parser.add_option('--no-squash', action='store_true',
3920 help='Don\'t squash multiple commits into one ' +
3921 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003922 parser.add_option('--email', default=None,
3923 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003924 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3925 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003926 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3927 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003928 help='Send the patchset to do a CQ dry run right after '
3929 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003930 parser.add_option('--dependencies', action='store_true',
3931 help='Uploads CLs of all the local branches that depend on '
3932 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003933
rmistry@google.com2dd99862015-06-22 12:22:18 +00003934 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003935 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003936 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003937 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003938 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003939 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003940 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003941
sbc@chromium.org71437c02015-04-09 19:29:40 +00003942 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003943 return 1
3944
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003945 options.reviewers = cleanup_list(options.reviewers)
3946 options.cc = cleanup_list(options.cc)
3947
tandriib80458a2016-06-23 12:20:07 -07003948 if options.message_file:
3949 if options.message:
3950 parser.error('only one of --message and --message-file allowed.')
3951 options.message = gclient_utils.FileRead(options.message_file)
3952 options.message_file = None
3953
tandrii4d0545a2016-07-06 03:56:49 -07003954 if options.cq_dry_run and options.use_commit_queue:
3955 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
3956
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003957 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3958 settings.GetIsGerrit()
3959
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003960 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00003961 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003962
3963
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003964def IsSubmoduleMergeCommit(ref):
3965 # When submodules are added to the repo, we expect there to be a single
3966 # non-git-svn merge commit at remote HEAD with a signature comment.
3967 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003968 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003969 return RunGit(cmd) != ''
3970
3971
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003972def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003973 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003974
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003975 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3976 upstream and closes the issue automatically and atomically.
3977
3978 Otherwise (in case of Rietveld):
3979 Squashes branch into a single commit.
3980 Updates changelog with metadata (e.g. pointer to review).
3981 Pushes/dcommits the code upstream.
3982 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003983 """
3984 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3985 help='bypass upload presubmit hook')
3986 parser.add_option('-m', dest='message',
3987 help="override review description")
3988 parser.add_option('-f', action='store_true', dest='force',
3989 help="force yes to questions (don't prompt)")
3990 parser.add_option('-c', dest='contributor',
3991 help="external contributor for patch (appended to " +
3992 "description and used as author for git). Should be " +
3993 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003994 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003995 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003996 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003997 auth_config = auth.extract_auth_config_from_options(options)
3998
3999 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004000
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004001 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4002 if cl.IsGerrit():
4003 if options.message:
4004 # This could be implemented, but it requires sending a new patch to
4005 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4006 # Besides, Gerrit has the ability to change the commit message on submit
4007 # automatically, thus there is no need to support this option (so far?).
4008 parser.error('-m MESSAGE option is not supported for Gerrit.')
4009 if options.contributor:
4010 parser.error(
4011 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4012 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4013 'the contributor\'s "name <email>". If you can\'t upload such a '
4014 'commit for review, contact your repository admin and request'
4015 '"Forge-Author" permission.')
4016 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4017 options.verbose)
4018
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004019 current = cl.GetBranch()
4020 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4021 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07004022 print()
4023 print('Attempting to push branch %r into another local branch!' % current)
4024 print()
4025 print('Either reparent this branch on top of origin/master:')
4026 print(' git reparent-branch --root')
4027 print()
4028 print('OR run `git rebase-update` if you think the parent branch is ')
4029 print('already committed.')
4030 print()
4031 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004032 return 1
4033
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004034 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004035 # Default to merging against our best guess of the upstream branch.
4036 args = [cl.GetUpstreamBranch()]
4037
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004038 if options.contributor:
4039 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07004040 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004041 return 1
4042
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004043 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004044 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004045
sbc@chromium.org71437c02015-04-09 19:29:40 +00004046 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004047 return 1
4048
4049 # This rev-list syntax means "show all commits not in my branch that
4050 # are in base_branch".
4051 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4052 base_branch]).splitlines()
4053 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004054 print('Base branch "%s" has %d commits '
4055 'not in this branch.' % (base_branch, len(upstream_commits)))
4056 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004057 return 1
4058
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004059 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004060 svn_head = None
4061 if cmd == 'dcommit' or base_has_submodules:
4062 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
4063 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004064
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004065 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004066 # If the base_head is a submodule merge commit, the first parent of the
4067 # base_head should be a git-svn commit, which is what we're interested in.
4068 base_svn_head = base_branch
4069 if base_has_submodules:
4070 base_svn_head += '^1'
4071
4072 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004073 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004074 print('This branch has %d additional commits not upstreamed yet.'
4075 % len(extra_commits.splitlines()))
4076 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
4077 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004078 return 1
4079
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004080 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004081 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004082 author = None
4083 if options.contributor:
4084 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004085 hook_results = cl.RunHook(
4086 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004087 may_prompt=not options.force,
4088 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004089 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004090 if not hook_results.should_continue():
4091 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004092
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004093 # Check the tree status if the tree status URL is set.
4094 status = GetTreeStatus()
4095 if 'closed' == status:
4096 print('The tree is closed. Please wait for it to reopen. Use '
4097 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4098 return 1
4099 elif 'unknown' == status:
4100 print('Unable to determine tree status. Please verify manually and '
4101 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4102 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004103
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004104 change_desc = ChangeDescription(options.message)
4105 if not change_desc.description and cl.GetIssue():
4106 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004107
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004108 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00004109 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004110 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00004111 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004112 print('No description set.')
4113 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00004114 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004115
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004116 # Keep a separate copy for the commit message, because the commit message
4117 # contains the link to the Rietveld issue, while the Rietveld message contains
4118 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004119 # Keep a separate copy for the commit message.
4120 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00004121 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004122
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004123 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00004124 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00004125 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00004126 # after it. Add a period on a new line to circumvent this. Also add a space
4127 # before the period to make sure that Gitiles continues to correctly resolve
4128 # the URL.
4129 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004130 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004131 commit_desc.append_footer('Patch from %s.' % options.contributor)
4132
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00004133 print('Description:')
4134 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004135
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004136 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004137 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00004138 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004139
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004140 # We want to squash all this branch's commits into one commit with the proper
4141 # description. We do this by doing a "reset --soft" to the base branch (which
4142 # keeps the working copy the same), then dcommitting that. If origin/master
4143 # has a submodule merge commit, we'll also need to cherry-pick the squashed
4144 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004145 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004146 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4147 # Delete the branches if they exist.
4148 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4149 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4150 result = RunGitWithCode(showref_cmd)
4151 if result[0] == 0:
4152 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004153
4154 # We might be in a directory that's present in this branch but not in the
4155 # trunk. Move up to the top of the tree so that git commands that expect a
4156 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004157 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004158 if rel_base_path:
4159 os.chdir(rel_base_path)
4160
4161 # Stuff our change into the merge branch.
4162 # We wrap in a try...finally block so if anything goes wrong,
4163 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004164 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004165 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004166 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004167 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004168 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00004169 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004170 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004171 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004172 RunGit(
4173 [
4174 'commit', '--author', options.contributor,
4175 '-m', commit_desc.description,
4176 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004177 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004178 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004179 if base_has_submodules:
4180 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
4181 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
4182 RunGit(['checkout', CHERRY_PICK_BRANCH])
4183 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004184 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004185 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004186 mirror = settings.GetGitMirror(remote)
4187 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004188 pending_prefix = settings.GetPendingRefPrefix()
4189 if not pending_prefix or branch.startswith(pending_prefix):
4190 # If not using refs/pending/heads/* at all, or target ref is already set
4191 # to pending, then push to the target ref directly.
4192 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004193 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004194 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004195 else:
4196 # Cherry-pick the change on top of pending ref and then push it.
4197 assert branch.startswith('refs/'), branch
4198 assert pending_prefix[-1] == '/', pending_prefix
4199 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004200 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004201 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004202 if retcode == 0:
4203 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004204 else:
4205 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004206 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00004207 'svn', 'dcommit',
4208 '-C%s' % options.similarity,
4209 '--no-rebase', '--rmdir',
4210 ]
4211 if settings.GetForceHttpsCommitUrl():
4212 # Allow forcing https commit URLs for some projects that don't allow
4213 # committing to http URLs (like Google Code).
4214 remote_url = cl.GetGitSvnRemoteUrl()
4215 if urlparse.urlparse(remote_url).scheme == 'http':
4216 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004217 cmd_args.append('--commit-url=%s' % remote_url)
4218 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004219 if 'Committed r' in output:
4220 revision = re.match(
4221 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4222 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004223 finally:
4224 # And then swap back to the original branch and clean up.
4225 RunGit(['checkout', '-q', cl.GetBranch()])
4226 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004227 if base_has_submodules:
4228 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004229
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004230 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004231 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004232 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004233
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004234 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004235 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004236 try:
4237 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4238 # We set pushed_to_pending to False, since it made it all the way to the
4239 # real ref.
4240 pushed_to_pending = False
4241 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004242 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004243
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004244 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004245 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004246 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004247 if not to_pending:
4248 if viewvc_url and revision:
4249 change_desc.append_footer(
4250 'Committed: %s%s' % (viewvc_url, revision))
4251 elif revision:
4252 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004253 print('Closing issue '
4254 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004255 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004256 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004257 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004258 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004259 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004260 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004261 if options.bypass_hooks:
4262 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4263 else:
4264 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004265 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004266
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004267 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004268 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004269 print('The commit is in the pending queue (%s).' % pending_ref)
4270 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4271 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004272
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004273 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4274 if os.path.isfile(hook):
4275 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004276
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004277 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004278
4279
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004280def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004281 print()
4282 print('Waiting for commit to be landed on %s...' % real_ref)
4283 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004284 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4285 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004286 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004287
4288 loop = 0
4289 while True:
4290 sys.stdout.write('fetching (%d)... \r' % loop)
4291 sys.stdout.flush()
4292 loop += 1
4293
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004294 if mirror:
4295 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004296 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4297 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4298 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4299 for commit in commits.splitlines():
4300 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004301 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004302 return commit
4303
4304 current_rev = to_rev
4305
4306
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004307def PushToGitPending(remote, pending_ref, upstream_ref):
4308 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4309
4310 Returns:
4311 (retcode of last operation, output log of last operation).
4312 """
4313 assert pending_ref.startswith('refs/'), pending_ref
4314 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4315 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4316 code = 0
4317 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004318 max_attempts = 3
4319 attempts_left = max_attempts
4320 while attempts_left:
4321 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004322 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004323 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004324
4325 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004326 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004327 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004328 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004329 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004330 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004331 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004332 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004333 continue
4334
4335 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004336 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004337 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004338 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004339 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004340 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4341 'the following files have merge conflicts:' % pending_ref)
4342 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4343 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004344 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004345 return code, out
4346
4347 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004348 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004349 code, out = RunGitWithCode(
4350 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4351 if code == 0:
4352 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004353 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004354 return code, out
4355
vapiera7fbd5a2016-06-16 09:17:49 -07004356 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004357 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004358 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004359 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004360 print('Fatal push error. Make sure your .netrc credentials and git '
4361 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004362 return code, out
4363
vapiera7fbd5a2016-06-16 09:17:49 -07004364 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004365 return code, out
4366
4367
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004368def IsFatalPushFailure(push_stdout):
4369 """True if retrying push won't help."""
4370 return '(prohibited by Gerrit)' in push_stdout
4371
4372
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004373@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004374def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004375 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004376 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004377 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004378 # If it looks like previous commits were mirrored with git-svn.
4379 message = """This repository appears to be a git-svn mirror, but no
4380upstream SVN master is set. You probably need to run 'git auto-svn' once."""
4381 else:
4382 message = """This doesn't appear to be an SVN repository.
4383If your project has a true, writeable git repository, you probably want to run
4384'git cl land' instead.
4385If your project has a git mirror of an upstream SVN master, you probably need
4386to run 'git svn init'.
4387
4388Using the wrong command might cause your commit to appear to succeed, and the
4389review to be closed, without actually landing upstream. If you choose to
4390proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004391 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004392 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
tandrii3bb82ff2016-06-17 07:36:36 -07004393 # TODO(tandrii): kill this post SVN migration with
4394 # https://codereview.chromium.org/2076683002
4395 print('WARNING: chrome infrastructure is migrating SVN repos to Git.\n'
4396 'Please let us know of this project you are committing to:'
4397 ' http://crbug.com/600451')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004398 return SendUpstream(parser, args, 'dcommit')
4399
4400
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004401@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004402def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004403 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004404 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004405 print('This appears to be an SVN repository.')
4406 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004407 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004408 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004409 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004410
4411
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004412@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004413def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004414 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004415 parser.add_option('-b', dest='newbranch',
4416 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004417 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004418 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004419 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4420 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004421 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004422 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004423 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004424 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004425 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004426 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004427
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004428
4429 group = optparse.OptionGroup(
4430 parser,
4431 'Options for continuing work on the current issue uploaded from a '
4432 'different clone (e.g. different machine). Must be used independently '
4433 'from the other options. No issue number should be specified, and the '
4434 'branch must have an issue number associated with it')
4435 group.add_option('--reapply', action='store_true', dest='reapply',
4436 help='Reset the branch and reapply the issue.\n'
4437 'CAUTION: This will undo any local changes in this '
4438 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004439
4440 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004441 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004442 parser.add_option_group(group)
4443
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004444 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004445 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004446 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004447 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004448 auth_config = auth.extract_auth_config_from_options(options)
4449
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004450
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004451 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004452 if options.newbranch:
4453 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004454 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004455 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004456
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004457 cl = Changelist(auth_config=auth_config,
4458 codereview=options.forced_codereview)
4459 if not cl.GetIssue():
4460 parser.error('current branch must have an associated issue')
4461
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004462 upstream = cl.GetUpstreamBranch()
4463 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004464 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004465
4466 RunGit(['reset', '--hard', upstream])
4467 if options.pull:
4468 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004469
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004470 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4471 options.directory)
4472
4473 if len(args) != 1 or not args[0]:
4474 parser.error('Must specify issue number or url')
4475
4476 # We don't want uncommitted changes mixed up with the patch.
4477 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004478 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004479
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004480 if options.newbranch:
4481 if options.force:
4482 RunGit(['branch', '-D', options.newbranch],
4483 stderr=subprocess2.PIPE, error_ok=True)
4484 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004485 elif not GetCurrentBranch():
4486 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004487
4488 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4489
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004490 if cl.IsGerrit():
4491 if options.reject:
4492 parser.error('--reject is not supported with Gerrit codereview.')
4493 if options.nocommit:
4494 parser.error('--nocommit is not supported with Gerrit codereview.')
4495 if options.directory:
4496 parser.error('--directory is not supported with Gerrit codereview.')
4497
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004498 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004499 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004500
4501
4502def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004503 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004504 # Provide a wrapper for git svn rebase to help avoid accidental
4505 # git svn dcommit.
4506 # It's the only command that doesn't use parser at all since we just defer
4507 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004508
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004509 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004510
4511
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004512def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004513 """Fetches the tree status and returns either 'open', 'closed',
4514 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004515 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004516 if url:
4517 status = urllib2.urlopen(url).read().lower()
4518 if status.find('closed') != -1 or status == '0':
4519 return 'closed'
4520 elif status.find('open') != -1 or status == '1':
4521 return 'open'
4522 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004523 return 'unset'
4524
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004525
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004526def GetTreeStatusReason():
4527 """Fetches the tree status from a json url and returns the message
4528 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004529 url = settings.GetTreeStatusUrl()
4530 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004531 connection = urllib2.urlopen(json_url)
4532 status = json.loads(connection.read())
4533 connection.close()
4534 return status['message']
4535
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004536
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004537def GetBuilderMaster(bot_list):
4538 """For a given builder, fetch the master from AE if available."""
4539 map_url = 'https://builders-map.appspot.com/'
4540 try:
4541 master_map = json.load(urllib2.urlopen(map_url))
4542 except urllib2.URLError as e:
4543 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4544 (map_url, e))
4545 except ValueError as e:
4546 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4547 if not master_map:
4548 return None, 'Failed to build master map.'
4549
4550 result_master = ''
4551 for bot in bot_list:
4552 builder = bot.split(':', 1)[0]
4553 master_list = master_map.get(builder, [])
4554 if not master_list:
4555 return None, ('No matching master for builder %s.' % builder)
4556 elif len(master_list) > 1:
4557 return None, ('The builder name %s exists in multiple masters %s.' %
4558 (builder, master_list))
4559 else:
4560 cur_master = master_list[0]
4561 if not result_master:
4562 result_master = cur_master
4563 elif result_master != cur_master:
4564 return None, 'The builders do not belong to the same master.'
4565 return result_master, None
4566
4567
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004568def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004569 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004570 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004571 status = GetTreeStatus()
4572 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004573 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004574 return 2
4575
vapiera7fbd5a2016-06-16 09:17:49 -07004576 print('The tree is %s' % status)
4577 print()
4578 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004579 if status != 'open':
4580 return 1
4581 return 0
4582
4583
maruel@chromium.org15192402012-09-06 12:38:29 +00004584def CMDtry(parser, args):
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004585 """Triggers try jobs through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00004586 group = optparse.OptionGroup(parser, "Try job options")
4587 group.add_option(
4588 "-b", "--bot", action="append",
4589 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
4590 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004591 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00004592 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004593 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00004594 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004595 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00004596 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004597 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004598 "-r", "--revision",
4599 help="Revision to use for the try job; default: the "
4600 "revision will be determined by the try server; see "
4601 "its waterfall for more info")
4602 group.add_option(
4603 "-c", "--clobber", action="store_true", default=False,
4604 help="Force a clobber before building; e.g. don't do an "
4605 "incremental build")
4606 group.add_option(
4607 "--project",
4608 help="Override which project to use. Projects are defined "
4609 "server-side to define what default bot set to use")
4610 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004611 "-p", "--property", dest="properties", action="append", default=[],
4612 help="Specify generic properties in the form -p key1=value1 -p "
4613 "key2=value2 etc (buildbucket only). The value will be treated as "
4614 "json if decodable, or as string otherwise.")
4615 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004616 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004617 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004618 "--use-rietveld", action="store_true", default=False,
4619 help="Use Rietveld to trigger try jobs.")
4620 group.add_option(
4621 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4622 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004623 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004624 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004625 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004626 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004627
machenbach@chromium.org45453142015-09-15 08:45:22 +00004628 if options.use_rietveld and options.properties:
4629 parser.error('Properties can only be specified with buildbucket')
4630
4631 # Make sure that all properties are prop=value pairs.
4632 bad_params = [x for x in options.properties if '=' not in x]
4633 if bad_params:
4634 parser.error('Got properties with missing "=": %s' % bad_params)
4635
maruel@chromium.org15192402012-09-06 12:38:29 +00004636 if args:
4637 parser.error('Unknown arguments: %s' % args)
4638
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004639 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004640 if not cl.GetIssue():
4641 parser.error('Need to upload first')
4642
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004643 if cl.IsGerrit():
4644 parser.error(
4645 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4646 'If your project has Commit Queue, dry run is a workaround:\n'
4647 ' git cl set-commit --dry-run')
4648 # Code below assumes Rietveld issue.
4649 # TODO(tandrii): actually implement for Gerrit http://crbug.com/599931.
4650
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004651 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004652 if props.get('closed'):
qyearsleyeab3c042016-08-24 09:18:28 -07004653 parser.error('Cannot send try jobs for a closed CL')
agable@chromium.org787e3062014-08-20 16:31:19 +00004654
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004655 if props.get('private'):
qyearsleyeab3c042016-08-24 09:18:28 -07004656 parser.error('Cannot use try bots with private issue')
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004657
maruel@chromium.org15192402012-09-06 12:38:29 +00004658 if not options.name:
4659 options.name = cl.GetBranch()
4660
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004661 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004662 options.master, err_msg = GetBuilderMaster(options.bot)
4663 if err_msg:
4664 parser.error('Tryserver master cannot be found because: %s\n'
4665 'Please manually specify the tryserver master'
4666 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004667
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004668 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004669 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004670 if not options.bot:
4671 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004672
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004673 # Get try masters from PRESUBMIT.py files.
4674 masters = presubmit_support.DoGetTryMasters(
4675 change,
4676 change.LocalPaths(),
4677 settings.GetRoot(),
4678 None,
4679 None,
4680 options.verbose,
4681 sys.stdout)
4682 if masters:
4683 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004684
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004685 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4686 options.bot = presubmit_support.DoGetTrySlaves(
4687 change,
4688 change.LocalPaths(),
4689 settings.GetRoot(),
4690 None,
4691 None,
4692 options.verbose,
4693 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004694
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004695 if not options.bot:
tandrii9de9ec62016-07-13 03:01:59 -07004696 return {}
maruel@chromium.org15192402012-09-06 12:38:29 +00004697
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004698 builders_and_tests = {}
4699 # TODO(machenbach): The old style command-line options don't support
4700 # multiple try masters yet.
4701 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4702 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4703
4704 for bot in old_style:
4705 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004706 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004707 elif ',' in bot:
4708 parser.error('Specify one bot per --bot flag')
4709 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004710 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004711
4712 for bot, tests in new_style:
4713 builders_and_tests.setdefault(bot, []).extend(tests)
4714
4715 # Return a master map with one master to be backwards compatible. The
4716 # master name defaults to an empty string, which will cause the master
4717 # not to be set on rietveld (deprecated).
4718 return {options.master: builders_and_tests}
4719
4720 masters = GetMasterMap()
tandrii9de9ec62016-07-13 03:01:59 -07004721 if not masters:
4722 # Default to triggering Dry Run (see http://crbug.com/625697).
4723 if options.verbose:
4724 print('git cl try with no bots now defaults to CQ Dry Run.')
4725 try:
4726 cl.SetCQState(_CQState.DRY_RUN)
4727 print('scheduled CQ Dry Run on %s' % cl.GetIssueURL())
4728 return 0
4729 except KeyboardInterrupt:
4730 raise
4731 except:
4732 print('WARNING: failed to trigger CQ Dry Run.\n'
4733 'Either:\n'
4734 ' * your project has no CQ\n'
4735 ' * you don\'t have permission to trigger Dry Run\n'
4736 ' * bug in this code (see stack trace below).\n'
4737 'Consider specifying which bots to trigger manually '
4738 'or asking your project owners for permissions '
4739 'or contacting Chrome Infrastructure team at '
4740 'https://www.chromium.org/infra\n\n')
4741 # Still raise exception so that stack trace is printed.
4742 raise
stip@chromium.org43064fd2013-12-18 20:07:44 +00004743
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004744 for builders in masters.itervalues():
4745 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004746 print('ERROR You are trying to send a job to a triggered bot. This type '
4747 'of bot requires an\ninitial job from a parent (usually a builder).'
4748 ' Instead send your job to the parent.\n'
4749 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004750 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004751
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004752 patchset = cl.GetMostRecentPatchset()
4753 if patchset and patchset != cl.GetPatchset():
4754 print(
4755 '\nWARNING Mismatch between local config and server. Did a previous '
4756 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4757 'Continuing using\npatchset %s.\n' % patchset)
nodirabb9b222016-07-29 14:23:20 -07004758 if not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004759 try:
4760 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4761 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004762 print('ERROR: %s' % ex)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004763 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004764 except Exception as e:
4765 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
qyearsleyeab3c042016-08-24 09:18:28 -07004766 print('ERROR: Exception when trying to trigger try jobs: %s\n%s' %
vapiera7fbd5a2016-06-16 09:17:49 -07004767 (e, stacktrace))
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004768 return 1
4769 else:
4770 try:
4771 cl.RpcServer().trigger_distributed_try_jobs(
4772 cl.GetIssue(), patchset, options.name, options.clobber,
4773 options.revision, masters)
4774 except urllib2.HTTPError as e:
4775 if e.code == 404:
4776 print('404 from rietveld; '
4777 'did you mean to use "git try" instead of "git cl try"?')
4778 return 1
4779 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004780
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004781 for (master, builders) in sorted(masters.iteritems()):
4782 if master:
vapiera7fbd5a2016-06-16 09:17:49 -07004783 print('Master: %s' % master)
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004784 length = max(len(builder) for builder in builders)
4785 for builder in sorted(builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004786 print(' %*s: %s' % (length, builder, ','.join(builders[builder])))
maruel@chromium.org15192402012-09-06 12:38:29 +00004787 return 0
4788
4789
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004790def CMDtry_results(parser, args):
4791 group = optparse.OptionGroup(parser, "Try job results options")
4792 group.add_option(
4793 "-p", "--patchset", type=int, help="patchset number if not current.")
4794 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004795 "--print-master", action='store_true', help="print master name as well.")
4796 group.add_option(
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004797 "--color", action='store_true', default=setup_color.IS_TTY,
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004798 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004799 group.add_option(
4800 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4801 help="Host of buildbucket. The default host is %default.")
qyearsley53f48a12016-09-01 10:45:13 -07004802 group.add_option(
4803 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004804 parser.add_option_group(group)
4805 auth.add_auth_options(parser)
4806 options, args = parser.parse_args(args)
4807 if args:
4808 parser.error('Unrecognized args: %s' % ' '.join(args))
4809
4810 auth_config = auth.extract_auth_config_from_options(options)
4811 cl = Changelist(auth_config=auth_config)
4812 if not cl.GetIssue():
4813 parser.error('Need to upload first')
4814
4815 if not options.patchset:
4816 options.patchset = cl.GetMostRecentPatchset()
4817 if options.patchset and options.patchset != cl.GetPatchset():
4818 print(
4819 '\nWARNING Mismatch between local config and server. Did a previous '
4820 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4821 'Continuing using\npatchset %s.\n' % options.patchset)
4822 try:
4823 jobs = fetch_try_jobs(auth_config, cl, options)
4824 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004825 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004826 return 1
4827 except Exception as e:
4828 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
qyearsleyeab3c042016-08-24 09:18:28 -07004829 print('ERROR: Exception when trying to fetch try jobs: %s\n%s' %
vapiera7fbd5a2016-06-16 09:17:49 -07004830 (e, stacktrace))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004831 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004832 if options.json:
4833 write_try_results_json(options.json, jobs)
4834 else:
4835 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004836 return 0
4837
4838
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004839@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004840def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004841 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004842 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004843 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004844 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004845
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004846 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004847 if args:
4848 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004849 branch = cl.GetBranch()
4850 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004851 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004852 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004853
4854 # Clear configured merge-base, if there is one.
4855 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004856 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004857 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004858 return 0
4859
4860
thestig@chromium.org00858c82013-12-02 23:08:03 +00004861def CMDweb(parser, args):
4862 """Opens the current CL in the web browser."""
4863 _, args = parser.parse_args(args)
4864 if args:
4865 parser.error('Unrecognized args: %s' % ' '.join(args))
4866
4867 issue_url = Changelist().GetIssueURL()
4868 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004869 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004870 return 1
4871
4872 webbrowser.open(issue_url)
4873 return 0
4874
4875
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004876def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004877 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004878 parser.add_option('-d', '--dry-run', action='store_true',
4879 help='trigger in dry run mode')
4880 parser.add_option('-c', '--clear', action='store_true',
4881 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004882 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004883 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004884 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004885 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004886 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004887 if args:
4888 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004889 if options.dry_run and options.clear:
4890 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4891
iannuccie53c9352016-08-17 14:40:40 -07004892 cl = Changelist(auth_config=auth_config, issue=options.issue,
4893 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004894 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07004895 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004896 elif options.dry_run:
4897 state = _CQState.DRY_RUN
4898 else:
4899 state = _CQState.COMMIT
4900 if not cl.GetIssue():
4901 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07004902 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004903 return 0
4904
4905
groby@chromium.org411034a2013-02-26 15:12:01 +00004906def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004907 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07004908 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004909 auth.add_auth_options(parser)
4910 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004911 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004912 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004913 if args:
4914 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07004915 cl = Changelist(auth_config=auth_config, issue=options.issue,
4916 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00004917 # Ensure there actually is an issue to close.
4918 cl.GetDescription()
4919 cl.CloseIssue()
4920 return 0
4921
4922
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004923def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004924 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07004925 parser.add_option(
4926 '--stat',
4927 action='store_true',
4928 dest='stat',
4929 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004930 auth.add_auth_options(parser)
4931 options, args = parser.parse_args(args)
4932 auth_config = auth.extract_auth_config_from_options(options)
4933 if args:
4934 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004935
4936 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004937 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004938 # Staged changes would be committed along with the patch from last
4939 # upload, hence counted toward the "last upload" side in the final
4940 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004941 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004942 return 1
4943
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004944 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004945 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004946 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004947 if not issue:
4948 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004949 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004950 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004951
4952 # Create a new branch based on the merge-base
4953 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004954 # Clear cached branch in cl object, to avoid overwriting original CL branch
4955 # properties.
4956 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004957 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004958 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004959 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004960 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004961 return rtn
4962
wychen@chromium.org06928532015-02-03 02:11:29 +00004963 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004964 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07004965 cmd = ['git', 'diff']
4966 if options.stat:
4967 cmd.append('--stat')
4968 cmd.extend([TMP_BRANCH, branch, '--'])
4969 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004970 finally:
4971 RunGit(['checkout', '-q', branch])
4972 RunGit(['branch', '-D', TMP_BRANCH])
4973
4974 return 0
4975
4976
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004977def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004978 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004979 parser.add_option(
4980 '--no-color',
4981 action='store_true',
4982 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004983 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004984 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004985 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004986
4987 author = RunGit(['config', 'user.email']).strip() or None
4988
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004989 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004990
4991 if args:
4992 if len(args) > 1:
4993 parser.error('Unknown args')
4994 base_branch = args[0]
4995 else:
4996 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004997 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004998
4999 change = cl.GetChange(base_branch, None)
5000 return owners_finder.OwnersFinder(
5001 [f.LocalPath() for f in
5002 cl.GetChange(base_branch, None).AffectedFiles()],
5003 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07005004 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005005 disable_color=options.no_color).run()
5006
5007
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005008def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005009 """Generates a diff command."""
5010 # Generate diff for the current branch's changes.
5011 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
5012 upstream_commit, '--' ]
5013
5014 if args:
5015 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005016 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005017 diff_cmd.append(arg)
5018 else:
5019 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005020
5021 return diff_cmd
5022
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005023def MatchingFileType(file_name, extensions):
5024 """Returns true if the file name ends with one of the given extensions."""
5025 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005026
enne@chromium.org555cfe42014-01-29 18:21:39 +00005027@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005028def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005029 """Runs auto-formatting tools (clang-format etc.) on the diff."""
zengsterbf470142016-07-07 16:43:00 -07005030 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005031 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005032 parser.add_option('--full', action='store_true',
5033 help='Reformat the full content of all touched files')
5034 parser.add_option('--dry-run', action='store_true',
5035 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005036 parser.add_option('--python', action='store_true',
5037 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005038 parser.add_option('--diff', action='store_true',
5039 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005040 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005041
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005042 # git diff generates paths against the root of the repository. Change
5043 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005044 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005045 if rel_base_path:
5046 os.chdir(rel_base_path)
5047
digit@chromium.org29e47272013-05-17 17:01:46 +00005048 # Grab the merge-base commit, i.e. the upstream commit of the current
5049 # branch when it was created or the last time it was rebased. This is
5050 # to cover the case where the user may have called "git fetch origin",
5051 # moving the origin branch to a newer commit, but hasn't rebased yet.
5052 upstream_commit = None
5053 cl = Changelist()
5054 upstream_branch = cl.GetUpstreamBranch()
5055 if upstream_branch:
5056 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5057 upstream_commit = upstream_commit.strip()
5058
5059 if not upstream_commit:
5060 DieWithError('Could not find base commit for this branch. '
5061 'Are you in detached state?')
5062
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005063 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5064 diff_output = RunGit(changed_files_cmd)
5065 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005066 # Filter out files deleted by this CL
5067 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005068
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005069 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5070 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5071 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005072 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005073
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005074 top_dir = os.path.normpath(
5075 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5076
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005077 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5078 # formatted. This is used to block during the presubmit.
5079 return_value = 0
5080
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005081 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005082 # Locate the clang-format binary in the checkout
5083 try:
5084 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005085 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005086 DieWithError(e)
5087
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005088 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005089 cmd = [clang_format_tool]
5090 if not opts.dry_run and not opts.diff:
5091 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005092 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005093 if opts.diff:
5094 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005095 else:
5096 env = os.environ.copy()
5097 env['PATH'] = str(os.path.dirname(clang_format_tool))
5098 try:
5099 script = clang_format.FindClangFormatScriptInChromiumTree(
5100 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005101 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005102 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005103
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005104 cmd = [sys.executable, script, '-p0']
5105 if not opts.dry_run and not opts.diff:
5106 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005107
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005108 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5109 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005110
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005111 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5112 if opts.diff:
5113 sys.stdout.write(stdout)
5114 if opts.dry_run and len(stdout) > 0:
5115 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005116
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005117 # Similar code to above, but using yapf on .py files rather than clang-format
5118 # on C/C++ files
5119 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005120 yapf_tool = gclient_utils.FindExecutable('yapf')
5121 if yapf_tool is None:
5122 DieWithError('yapf not found in PATH')
5123
5124 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005125 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005126 cmd = [yapf_tool]
5127 if not opts.dry_run and not opts.diff:
5128 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005129 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005130 if opts.diff:
5131 sys.stdout.write(stdout)
5132 else:
5133 # TODO(sbc): yapf --lines mode still has some issues.
5134 # https://github.com/google/yapf/issues/154
5135 DieWithError('--python currently only works with --full')
5136
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005137 # Dart's formatter does not have the nice property of only operating on
5138 # modified chunks, so hard code full.
5139 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005140 try:
5141 command = [dart_format.FindDartFmtToolInChromiumTree()]
5142 if not opts.dry_run and not opts.diff:
5143 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005144 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005145
ppi@chromium.org6593d932016-03-03 15:41:15 +00005146 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005147 if opts.dry_run and stdout:
5148 return_value = 2
5149 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005150 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5151 'found in this checkout. Files in other languages are still '
5152 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005153
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005154 # Format GN build files. Always run on full build files for canonical form.
5155 if gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005156 cmd = ['gn', 'format' ]
5157 if opts.dry_run or opts.diff:
5158 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005159 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005160 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5161 shell=sys.platform == 'win32',
5162 cwd=top_dir)
5163 if opts.dry_run and gn_ret == 2:
5164 return_value = 2 # Not formatted.
5165 elif opts.diff and gn_ret == 2:
5166 # TODO this should compute and print the actual diff.
5167 print("This change has GN build file diff for " + gn_diff_file)
5168 elif gn_ret != 0:
5169 # For non-dry run cases (and non-2 return values for dry-run), a
5170 # nonzero error code indicates a failure, probably because the file
5171 # doesn't parse.
5172 DieWithError("gn format failed on " + gn_diff_file +
5173 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005174
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005175 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005176
5177
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005178@subcommand.usage('<codereview url or issue id>')
5179def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005180 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005181 _, args = parser.parse_args(args)
5182
5183 if len(args) != 1:
5184 parser.print_help()
5185 return 1
5186
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005187 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005188 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005189 parser.print_help()
5190 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005191 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005192
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005193 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005194 output = RunGit(['config', '--local', '--get-regexp',
5195 r'branch\..*\.%s' % issueprefix],
5196 error_ok=True)
5197 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005198 if issue == target_issue:
5199 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005200
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005201 branches = []
5202 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005203 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005204 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005205 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005206 return 1
5207 if len(branches) == 1:
5208 RunGit(['checkout', branches[0]])
5209 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005210 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005211 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005212 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005213 which = raw_input('Choose by index: ')
5214 try:
5215 RunGit(['checkout', branches[int(which)]])
5216 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005217 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005218 return 1
5219
5220 return 0
5221
5222
maruel@chromium.org29404b52014-09-08 22:58:00 +00005223def CMDlol(parser, args):
5224 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005225 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005226 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5227 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5228 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005229 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005230 return 0
5231
5232
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005233class OptionParser(optparse.OptionParser):
5234 """Creates the option parse and add --verbose support."""
5235 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005236 optparse.OptionParser.__init__(
5237 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005238 self.add_option(
5239 '-v', '--verbose', action='count', default=0,
5240 help='Use 2 times for more debugging info')
5241
5242 def parse_args(self, args=None, values=None):
5243 options, args = optparse.OptionParser.parse_args(self, args, values)
5244 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5245 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5246 return options, args
5247
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005248
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005249def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005250 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005251 print('\nYour python version %s is unsupported, please upgrade.\n' %
5252 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005253 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005254
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005255 # Reload settings.
5256 global settings
5257 settings = Settings()
5258
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005259 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005260 dispatcher = subcommand.CommandDispatcher(__name__)
5261 try:
5262 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005263 except auth.AuthenticationError as e:
5264 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005265 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005266 if e.code != 500:
5267 raise
5268 DieWithError(
5269 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5270 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005271 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005272
5273
5274if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005275 # These affect sys.stdout so do it outside of main() to simplify mocks in
5276 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005277 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005278 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005279 try:
5280 sys.exit(main(sys.argv[1:]))
5281 except KeyboardInterrupt:
5282 sys.stderr.write('interrupted\n')
5283 sys.exit(1)